Building Routable Road-Network Graphs from HD Maps

A planner does not route on lane polylines — it routes on a weighted directed graph, and the quality of that graph decides whether the vehicle can find a legal path at all. Building it from an HD map means turning the lane geometry and lane-level topology into nodes and edges with the right granularity, the right edge types, and cost weights that make legal, comfortable paths cheap. This sits at the routing end of the lane geometry extraction and road network processing domain, downstream of centerline and attribute extraction. The failure that matters most is an unreachable island of lanes, so the whole construction is gated on a network-wide reachability proof.

Lane geometry and topology become a weighted graph with three edge types, gated on reachability:

Road-Network Graph Construction Pipeline Input box of lane geometry and topology. Arrow to a node-granularity box. Arrow to an edges box listing successor, lane-change, and turn edges. Arrow to a cost-assignment box. Arrow to a diamond reachability gate. Pass leads to a routable graph terminal; fail leads to a flag isolated components box. Lane geometry + topology centerlines · successor links Node granularity lane · or lane-segment Edges: successor · lane-change · turn + costs: length, turn & change penalties Reachable? all lanes connected yes Routable graph no Flag isolated lanes

Representation Overview #

The core modelling choice is node granularity, and it trades graph size against the fidelity of the lane-change model:

Representation Node = Lane changes Graph size Best fit
Lane-as-node Whole lane Only at lane ends Smallest Coarse routing, highway networks
Lane-segment-as-node Fixed arc-length slice Mid-lane, at any segment Larger (×segments) Urban, continuous lane-change planning
Waypoint-as-node Dense centerline sample Continuous Largest Fine motion planning, short horizons

Lane-as-node is compact but cannot express a lane change that begins partway along a lane; lane-segment-as-node splits each lane at a fixed step so lateral edges can attach mid-lane. Waypoint granularity is rarely used for network routing — it belongs to the local planner. Most production routers use lane-segment nodes with the step sized to the shortest realistic lane-change distance.

Stage-by-Stage Implementation #

Stage 1 — Emit nodes at the chosen granularity #

Split each lane centerline into arc-length segments and create a node per segment. Carry the lane id and the arc-length interval so edges can be attached precisely.

python
import networkx as nx
import numpy as np

def add_segment_nodes(g: nx.DiGraph, lane_id, centerline: np.ndarray, step=15.0):
    """One node per `step`-metre slice of the lane."""
    s = np.concatenate([[0.0], np.cumsum(np.linalg.norm(np.diff(centerline, axis=0), axis=1))])
    breaks = np.arange(0.0, s[-1], step)
    for i, s0 in enumerate(breaks):
        g.add_node((lane_id, i), lane=lane_id, s_start=float(s0),
                   s_end=float(min(s0 + step, s[-1])))

Key parameter: step is the segment length — smaller steps allow finer lane-change attachment at the cost of a bigger graph.

Stage 2 — Add the three edge types #

Successor edges run along a lane and across successor links; lane-change edges connect laterally adjacent segments where a change is legal; turn edges cross junctions from the lane graph.

python
def add_edges(g, lane_graph, adjacency, legal_change):
    # longitudinal successors within and across lanes
    for u, v in lane_graph.edges:
        g.add_edge(u, v, kind="successor", base=_length(g, u))
    # lateral lane changes where legal
    for a, b in adjacency:            # laterally adjacent segment pairs
        if legal_change(a, b):
            g.add_edge(a, b, kind="lane_change", base=_length(g, a))

Key parameter: legal_change consults lane markings and regulatory attributes — a lane change is added only where the map permits it, never from geometric adjacency alone.

Stage 3 — Assign edge costs #

Weight each edge so routing prefers legal, comfortable paths. Cost combines physical length with penalties that discourage unnecessary turns and lane changes.

python
def edge_cost(kind, length, turn_penalty=8.0, change_penalty=20.0):
    c = length
    if kind == "turn":
        c += turn_penalty
    elif kind == "lane_change":
        c += change_penalty
    return c

Key parameters: turn_penalty and change_penalty are additive costs in metre-equivalents; raising change_penalty makes the router hold a lane longer before changing.

Validation & QC Automation #

The decisive gate is reachability. Also validate that costs are positive and every node has consistent degree.

  • Reachability: every drivable lane node is in the same weakly connected component as the network core; isolated components are flagged with their tile ids.
  • No zero/negative costs: every edge weight > 0 so shortest-path algorithms terminate correctly.
  • Turn coverage: every junction in the lane successor graph produced at least one turn edge.
python
def assert_reachable(g: nx.DiGraph):
    comps = list(nx.weakly_connected_components(g))
    comps.sort(key=len, reverse=True)
    isolated = [c for c in comps[1:] if len(c) < 3]  # small islands
    assert not isolated, f"{len(isolated)} isolated lane island(s)"
    assert all(d["base"] > 0 for *_, d in g.edges(data=True)), "non-positive cost"

Edge Cases & Failure Patterns #

  • Tile-border islands. A lane whose successor lives in an unloaded neighbouring tile looks isolated. Stitch cross-tile links before the reachability check, using the boundary handling from managing map tile boundaries in ROS2.
  • Illegal lane changes from geometry. Adding lateral edges from adjacency alone lets the router cross a solid line. Gate every lane-change edge on the marking/regulatory attribute.
  • Negative-cycle from bad costs. A mis-signed penalty can create a zero or negative edge, breaking Dijkstra. Assert strictly positive weights.
  • Over-penalized turns. Turn penalties set too high make the router take absurd detours to avoid a legal turn. Calibrate penalties against known good routes.

Performance & Scale Notes #

Segment granularity is the dominant cost driver: halving step roughly doubles node and edge counts. Choose the largest step that still supports the planner's lane-change model. Build per tile and stitch at borders so the whole-map graph never has to be resident at once, and store it in a compressed sparse form for the router. Reachability analysis is a single linear-time connected-components pass; run it after stitching. The construction parallelizes cleanly per tile — the same batch pattern used across the lane geometry extraction domain.

FAQ #

Should a lane be one node or many? #

It depends on where lane changes are allowed. Lane-as-node is compact and fine when lane changes only happen at lane boundaries, but it cannot express a change that begins partway along a lane. Lane-segment-as-node splits each lane at regular arc-length intervals so lateral edges can connect mid-lane, at the cost of a larger graph. Most planners that model continuous lane changes use the segment representation.

How are lane-change edges different from successor edges? #

A successor edge is longitudinal: it connects the end of a lane to the start of the next lane along the direction of travel. A lane-change edge is lateral: it connects a point on one lane to a laterally adjacent point on a parallel lane in the same direction. Lateral edges carry a higher cost and are only added where a change is legal, which the lane markings and regulatory attributes determine, not geometry alone.

Why validate reachability separately from topology? #

Topology validation confirms links are well-formed; reachability confirms the graph is usable. A map can have perfectly valid links yet contain an island — a pocket of lanes connected to each other but not to the rest of the network, often a mapping gap at a tile border. Only a connectivity analysis over the whole graph finds those islands.

Up one level: Lane Geometry Extraction & Road Network Processing — the parent domain whose routing layer this construction produces.