Skip to content

Stable edge ids

edge_id = (hash(osm_id, source, target) >> 1)::BIGINT      -- DuckDB's hash()
  • osm_id: the OSM way the edge comes from.
  • source, target: the OSM node ids where the edge starts and ends (or a virtual node). Their order is the direction, so the two directions of a two-way road have different ids.

edge_id is not a row number. All three inputs come from OpenStreetMap ids, so an unchanged edge gets the same edge_id on every rebuild, in every area cut from the same build, and in every export. An id changes only when its edge changes: a new way id, new end nodes, a new junction that splits it, or, for a merged edge, a change in the pieces it is made of.

The is_reverse column says whether an edge runs against the way's drawing direction (a way tagged oneway=-1 is turned round first, so its one edge is not a reverse). For a merged two-way edge the forward direction is chosen by node id, not by the drawing. It is not part of the id.

One id per edge

(osm_id, source, target) must be unique. Where it would repeat (a way that ends where it starts, a way that runs between the same two junctions twice, or goes from A to B and back), the build splits the edge at its midpoint with a virtual node. A virtual node has node_id < 0 and its id is a hash of the edge's content, so it is as stable as the rest. If a repeated triple still survives, the build stops with an error. Monaco: 1 virtual node in driving, 19 in walking, 21 in cycling.

The same id in every mode

Every mode cuts ways at one shared set of points, main.global_junctions: each node of a road that another highway way also uses (another road, or a footway, path or cycleway, such as a crosswalk), and each node where a road ends. It is made once from all highway ways in raw.ways, before any mode filters them. A road is cut at these points, and so is a footway or path that passes through one, even in a mode that doesn't have the road. So a way that is in two modes is cut into the same pieces and has the same edge_id in both. Monaco: 2,754 of the 2,765 driving edges have the same edge_id in cycling, 2,275 in walking. The others are roads that mode doesn't keep.

So a crosswalk cuts the road it crosses in driving too, and the road cuts the crosswalk in walking too (where a road whose sidewalk is a separate way isn't in the walking network): walkers and cyclists connect to the road there, and the pieces are the same in every mode. Ways that don't exist on the ground (highway=proposed, construction, …) cut nothing. options.global_junctions: false makes each mode split at its own junctions, and the ids of a road can then differ between modes.

Because the mode is not in the id, the same edge_id can be in several schemas. Across modes, the key is (mode, edge_id).

Merged edges

With options.merge_segments (on), a chain of edges that is one road is joined into one edge: the edges must agree on highway, name, oneway, maxspeed, lanes, surface, access, junction, layer, bridge, tunnel and service. Both directions take the osm_id of the piece at the forward edge's source end (seq 1).

A point in main.global_junctions is never merged away (with options.global_junctions on). So roads are not joined across OSM ways; only chains of footways, paths and the like are. Monaco:

Mode Edges with merging Edges with merge_segments: false
driving 2,765 2,765
walking 10,952 11,374
cycling 10,274 10,828

edge_id_map

Each mode with merging has an edge_id_map table: one row per piece of each merged edge. The reverse of a two-way road is a merged edge of its own, with its own rows. A piece and the merged edge it maps to always run the same way.

Column Meaning
old_edge_id the piece's edge_id in a build without merging
new_edge_id the merged edge it is part of
seq the piece's position, 1 at the forward edge's source end
is_reverse whether the piece (old_edge_id) runs against its way's drawing
osm_id the piece's own OSM way

An edge that is not in the table was not merged. Rows of edges removed by the component filter are removed too. Monaco, walking: 796 rows for 372 merged edges. The OSM ways of one merged edge, in order:

SELECT seq, osm_id FROM walking.edge_id_map WHERE new_edge_id = ? ORDER BY seq;

Connector edges

The edges that join a dangling path to the network have a negative osm_id: -min(a, b), where a and b are the osm_ids of the path and of the road it joins. Their edge_id uses the same formula. Select them with osm_id < 0. See Network clean-up.

Clips and exports

A clip (source.type: duckdb, build --source-db, duckosm extract) copies edges as they are, so every edge_id is the parent's. A clip has no edge_id_map. The road exports keep edge_id: as the id (SUMO, MATSim, GMNS link_id, OpenDRIVE road id) or as an attribute or tag (GeoPackage, Lanelet2 duckosm:edge_id). railML (rail) has none.

Compute or match an id

Every database has two macros:

Macro Formula
edge_id_hash(osm_id, source, target) the formula above
edge_id_hash_v1(osm_id, source, target, is_reverse) the older formula, which also hashed is_reverse: to map ids from a database built with it
SELECT count(*) FROM driving.edges WHERE edge_id <> edge_id_hash(osm_id, source, target);   -- 0

In Python: from duckosm import edge_id_hash, edge_id_expr (one id, or the SQL expression).

hash() is internal to DuckDB and may change in a new major version, so duckOSM requires duckdb<2. Outside DuckDB the id can't be recomputed. To give your own data the ids, join on the three columns and read edge_id:

SELECT s.*, e.edge_id
FROM my_segments s JOIN driving.edges e USING (osm_id, source, target);