How the routing graph is built
The method by which Lightshadow turns OpenStreetMap data into the graph it computes walking routes on. This is a technical description of the code that does it, not an explanation of the product.
What this page is for
Lightshadow's streets come from OpenStreetMap, a database published under the Open Database License (ODbL). When someone extracts part of that database, filters it and transforms it into something else — here, a routing graph — the licence calls the result a "Derivative Database", and whatever is shown to the public on the strength of it (the routes in the app) a "Produced Work".
The licence says that whoever publicly uses a Produced Work made from a Derivative Database must also offer its recipients, free of charge over the internet and in a machine-readable form, one of two things: either the whole Derivative Database, or a file containing all of the alterations made to the original database, or the method of making them — the licence gives "an algorithm" as its example — including any added content. That is section 4.6 of the ODbL, and the two options are its clauses (a) and (b). This page is option (b): it describes the complete method, with the exact filters, tags and constants the code uses. The attribution notice that section 4.3 requires on the work itself is in the app, on the "Licences" screen, and at the foot of this page.
1. What is fetched from OpenStreetMap, and how
The Overpass API is asked for the set of ways (way) in the city whose
highway tag has one of these eighteen values:
residential living_street pedestrian footway path steps service unclassified tertiary tertiary_link secondary secondary_link primary primary_link cycleway track bridleway corridor
The list is the routing engine's HIGHWAY_WALK constant, and it is the same
list that later decides which ways are walkable. The query, as sent for each block of the
city, is:
[out:json][timeout:180]; way["highway"~"^(residential|living_street|…|corridor)$"](S,W,N,E); out geom;
The city's bounding box is cut into blocks 0.02° on a side (about 2 km) and each block
is requested separately, with a 4 s pause between requests, trying three Overpass mirrors in
turn. out geom returns each way with the coordinates of its nodes; a way whose
node list and geometry differ in length (one clipped by the block edge) is dropped.
Only ways are requested: no standalone nodes and no relations, so a pedestrian
crossing mapped as a node (highway=crossing on a node) is not in the
data.
packages/api/tools/fetch_full_ways.py (query, blocks, mirrors); packages/api/tools/build_ways.py (artefact).
Which tags are kept
For each way, its OSM id, its list of node ids, its coordinates and these ten tags are kept; everything else is dropped:
highway foot access bicycle name name:ca name:es footway crossing bridge
The result is written to a compressed file (valencia_ways.json.gz) that ships
with the service. The service never queries OpenStreetMap live: everything it knows about
the streets is in that file.
2. How ways become a graph
- Walkability filter. A way is dropped if its
highwayis not in the list above, if it carriesaccess=private, or if it carriesfoot=no. (In bicycle mode, which the app does not offer, the list differs andbicycle=nois dropped.) - Projection. Coordinates are converted to metres with an equirectangular projection centred on the city.
- Splitting into edges. The number of ways using each node is counted. Each way is cut at every node it shares with another way (and at its last node): each piece is an edge of the graph and its two ends are nodes. A piece shorter than 0.5 m is dropped. A graph node keeps the OSM node id.
- Largest component. Connected components are labelled and the largest is recorded. When the nearest node to an origin or destination is looked up, only nodes of that component are considered, so a disconnected island never receives a route.
Each edge stores: its two nodes, the polyline in metres and in degrees, its length, a
sun fraction (which does not come from OSM, see §3) and an on_deck flag (§3). The
service additionally records, per edge, the OSM way it came from — its name
(name, and name:es for the Castilian form) and its
highway class — so that each stretch can be named in the directions.
packages/engine/sombra_engine/routing.py — _is_usable, build_graph, _label_components, nearest_node; packages/engine/sombra_engine/geo.py — Projection.
Measured on the Valencia artefact served today
| quantity | value |
|---|---|
| ways fetched | 67,152 (classes present: 17 of 18; no bridleway inside the box) |
| walkable ways | 66,308 (554 dropped for access=private, 290 more for foot=no) |
| nodes / edges | 108,622 / 158,896, 6,372.4 km of edge |
Ways: counted in packages/api/data/valencia_ways.json.gz (fetched on 30 September 2026) with the routing._is_usable filter. Nodes, edges and kilometres: the graph build of that same artefact, 30 September 2026.
3. What is derived per edge
Sun fraction
Not from OSM: it is computed separately, on every route request. This page describes only how the graph is obtained from OpenStreetMap.
Side of the street
Edges fall into two classes by their highway tag. If the way is a
carriageway (residential, living_street, service, unclassified, tertiary,
tertiary_link, secondary, secondary_link, primary, primary_link, road), it has two
pavements and the route search chooses a side. If the way is already the walking surface
(footway, path, steps, pedestrian, cycleway, track, corridor, bridleway),
there is no side to choose.
Where crossing is possible
The pavement is changed only at a node where OSM maps a pedestrian crossing, and the
change costs 12 equivalent metres; at no other node is the street crossed. A node counts as
a crossing if it belongs to a way tagged footway=crossing or carrying any value
of crossing=*; both tags are checked because in this artefact 9,384 ways carry
crossing=* against 8,481 with footway=crossing. Since only ways
were fetched, crossings mapped as nodes do not count (§1).
Bridge decks (on_deck)
The engine marks an edge as "on a deck" if its way carries bridge=* with any
value other than no, abandoned, dismantled, razed, demolished. In the
artefact served today, 876 ways carry such a value.
routing.py — on_bridge, route; sombra_api/artefacts.py — CARRIAGEWAY_CLASSES, _is_crossing_way, _derive_crossing_nodes; sombra_api/config.py — CROSSING_PENALTY_M 12.
4. Date of the data
The ways artefact records in its header the date of the fetch (fetched_utc),
the tool that made it, the number of blocks, the resulting bounding box and the list of
highway classes actually present, computed from the content rather than written
by hand. For Valencia, the artefact served today:
| fetched_utc | 2026-09-30T10:55:36Z |
| fetched_by | packages/api/tools/fetch_full_ways.py |
| chunk_files | 125 |
| bbox | −0.4441 39.2635 → −0.2757 39.5821 |
| way_count | 67,152 |
What that date is, exactly. fetched_utc is the
modification time of the newest cache block, that is, the moment of the download. The
Overpass response also carries the timestamp of the OSM database itself
(osm3s.timestamp_osm_base), but the cache stores only the elements, not that
header, so the OSM database state date is not recorded; the fetch time
is its upper bound. This is a known gap in the method.
build_ways.py — fetched_utc = mtime of the newest block; fetch_full_ways.py — the cache stores r.json()["elements"].
Credits and licence
- Street data © OpenStreetMap contributors, available under the Open Database License (ODbL) — openstreetmap.org/copyright. The routing graph described here is a Derivative Database of OpenStreetMap, and this page is the method of derivation that section 4.6(b) of the ODbL asks to be published.