Skip to main content

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 .

score = Σ |bearing(nodeᵢ₊₁) - bearing(nodeᵢ)| for all turns ≥ 5°
PropertyValue
Unitdegrees of accumulated turning
Typical range300 – 15 000
Default threshold300 (Preset: Mixed), 600 (Scenic), 1 000 (Twisty)
Best forFinding 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
PropertyValue
Unitdegrees of turning per kilometre
Typical range50 – 500
Default threshold50 (Mixed), 100 (Scenic), 160 (Twisty)
Best forComparing 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
PropertyValue
Unitinverse-radius (dimensionless)
Typical range10 – 200
Default threshold10 (Mixed), 20 (Scenic), 35 (Twisty)
Best forRoads 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)
PresetTotalDensityRadius
Mixed300~48~10
Scenic600~96~21
Twisty1 000~160~35

Parameters you can tune from the app

All of the following are surfaced in the Find Curvy Roads sheet (Search → Advanced):

ControlWhat it doesDefault
Scoring methodSwitch between Total / Density / RadiusTotal
PresetBaseline curvature threshold (Twisty / Scenic / Mixed)Mixed
Search radiusArea around GPS position or map centre50 km
Min curvature (override)Directly override the preset threshold(preset)
Road classesInclude/exclude primary, secondary, tertiary, unclassifiedsecondary + tertiary + unclassified
SurfacePaved / unknown / unpavedpaved + unknown
Unpaved score factorMultiply unpaved roads' score before filtering (0 = exclude entirely)0.70
Min sinuosityRatio of road length to crow-flies distance; filters arrow-straight roads that just happen to be long1.2
Min lengthMinimum segment length3 km
Max population densityExcludes roads near towns (100 = village, 500 = urban)500
Elevation dataEnrich results with elevation gain / range (requires server DEM)off
Scenic roads onlyFilter to officially marked scenic / tourist routesoff
Near water onlyFjords, lakes, rivers within 500 moff
Min elevation changeMountain-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:

ParameterCurrent stateWhy it needs backend work
min_turn_degHardcoded to in the ingestion scriptsRequires re-indexing the DB with a different threshold; no live API param
Max segment lengthCLI flag --max-segment-km on ingestion scripts onlyNot exposed via the /curvature/ingest-osm API
"Query all 3 algorithms simultaneously"No endpoint merges across DBsWould 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.