Curvy Roads — Scoring Algorithms
FastGIS scores every road segment using one of three curvature algorithms. Each algorithm
produces a separate SQLite database on the server (roads_total.db, roads_bearing.db,
roads_radius.db) and operates on a different numerical scale, so the thresholds shown
in the app are automatically adjusted when you switch methods.
The three algorithms
Total — Σ|Δbearing|
The default algorithm, compatible with the roadcurvature.com dataset.
For every consecutive pair of nodes along a road segment it computes the change in compass bearing and accumulates the absolute values, ignoring micro-jitter below 5°.
score = Σ |bearing(nodeᵢ₊₁) - bearing(nodeᵢ)| for all turns ≥ 5°
| Property | Value |
|---|---|
| Unit | degrees of accumulated turning |
| Typical range | 300 – 15 000 |
| Default threshold | 300 (Preset: Mixed), 600 (Scenic), 1 000 (Twisty) |
| Best for | Finding the most dramatic roads — long sequences of tight corners |
When to choose it: You want the all-time classics — roads where every kilometre adds up to a significant physical journey. A 60 km pass with consistent bends will score higher than a 5 km hairpin climb.
Density — Σ|Δbearing| / km
The Total score divided by the road's length in kilometres. Normalises for road length so short and long roads are compared on equal footing.
score = (Σ |Δbearing|) / distance_km
| Property | Value |
|---|---|
| Unit | degrees of turning per kilometre |
| Typical range | 50 – 500 |
| Default threshold | 50 (Mixed), 100 (Scenic), 160 (Twisty) |
| Best for | Comparing roads of different lengths fairly |
When to choose it: A tight 4 km hairpin road and a sprawling 80 km mountain pass with the same corner frequency will receive identical scores. Use this when you want to find the most intensely curvy kilometres regardless of how long the road is.
Radius — Σ(100 / R)
Computes the geometric circumradius at each node triple (A, B, C) — the radius of the
circle that passes through those three points. Roads with tighter corners have smaller radii
and therefore higher scores.
score = Σ (100 / R_km) for all node triples where R < 10 km
| Property | Value |
|---|---|
| Unit | inverse-radius (dimensionless) |
| Typical range | 10 – 200 |
| Default threshold | 10 (Mixed), 20 (Scenic), 35 (Twisty) |
| Best for | Roads dominated by technically tight corners (hairpin climbs, mountain passes) |
When to choose it: You care about the sharpness of individual corners more than accumulated bending. A road with three very tight hairpins but straight sections in between will rank higher here than under Total.
Score scale comparison
Because each algorithm operates on a different numerical range, the presets automatically scale the min_curvature threshold when you switch method. You don't need to tune it manually — a "Twisty" search with Total (≥ 1 000) and a "Twisty" search with Radius (≥ 35) will return roads of equivalent "twistiness" as judged by each respective measure.
scaled_threshold = default_for_algorithm × (preset_total_threshold / 1000)
| Preset | Total | Density | Radius |
|---|---|---|---|
| Mixed | 300 | ~48 | ~10 |
| Scenic | 600 | ~96 | ~21 |
| Twisty | 1 000 | ~160 | ~35 |
Parameters you can tune from the app
All of the following are surfaced in the Find Curvy Roads sheet (Search → Advanced):
| Control | What it does | Default |
|---|---|---|
| Scoring method | Switch between Total / Density / Radius | Total |
| Preset | Baseline curvature threshold (Twisty / Scenic / Mixed) | Mixed |
| Search radius | Area around GPS position or map centre | 50 km |
| Min curvature (override) | Directly override the preset threshold | (preset) |
| Road classes | Include/exclude primary, secondary, tertiary, unclassified | secondary + tertiary + unclassified |
| Surface | Paved / unknown / unpaved | paved + unknown |
| Unpaved score factor | Multiply unpaved roads' score before filtering (0 = exclude entirely) | 0.70 |
| Min sinuosity | Ratio of road length to crow-flies distance; filters arrow-straight roads that just happen to be long | 1.2 |
| Min length | Minimum segment length | 3 km |
| Max population density | Excludes roads near towns (100 = village, 500 = urban) | 500 |
| Elevation data | Enrich results with elevation gain / range (requires server DEM) | off |
| Scenic roads only | Filter to officially marked scenic / tourist routes | off |
| Near water only | Fjords, lakes, rivers within 500 m | off |
| Min elevation change | Mountain-pass filter (0 = no filter) | 0 m |
Parameters that require backend work
The following are currently hardcoded or only available as admin/ingestion-time settings:
| Parameter | Current state | Why it needs backend work |
|---|---|---|
min_turn_deg | Hardcoded to 5° in the ingestion scripts | Requires re-indexing the DB with a different threshold; no live API param |
| Max segment length | CLI flag --max-segment-km on ingestion scripts only | Not exposed via the /curvature/ingest-osm API |
| "Query all 3 algorithms simultaneously" | No endpoint merges across DBs | Would need a new server endpoint to union results and de-duplicate |
How the databases are built
The server maintains up to three separate SQLite databases, one per algorithm:
/data/curvature/roads_total.db ← default; compatible with roadcurvature.com KMZ
/data/curvature/roads_bearing.db
/data/curvature/roads_radius.db
Ingestion sources
KMZ (roadcurvature.com) — only populates roads_total.db:
POST /grass/curvature/ingest-kmz
Imports pre-computed KMZ files (norway.c_300.kmz, etc.) and filters by road type and surface class.
OSM — populates any of the three databases:
POST /grass/curvature/ingest-osm?method=total|bearing|radius
Fetches geometries from the Overpass API or a local PBF extract, computes curvature using the requested algorithm, and merges results into the relevant DB.
All three databases use the same schema and are enriched by the same optional post-processing
scripts (enrich_elevation_wcs.py, enrich_water_osm.py, enrich_population.py), so
elevation, water, and scenic filters work identically across all three algorithms.
Offline / tiled queries
The mobile app downloads curvature tiles from:
GET /grass/curvature/tiles/{algorithm}/{z}/{x}/{y}.json
Each algorithm (total, bearing, radius) has its own tile set. When you switch the
scoring method in the app, the offline query automatically reads from the matching tile set
if it has been downloaded, otherwise falls back to the live server.
See Offline Data for how to manage tile downloads.