Most connectivity gaps yield to a sensible snapping tolerance. A residual few do not, and they are always the same three shapes: a ferry whose terminal has no mapped link to the road network, a tunnel or bridge severed by an extract boundary, and a level-separated crossing that a naive proximity check wants to join and must not. This page covers all three, as the repair half of graph fragmentation prevention in OSM data within OSM Graph Architecture & Network Modeling, picking up from the fragments surfaced by auditing connectivity after every graph rebuild.
The discipline that matters is classification before repair. Two of these shapes want fixing and one wants leaving alone, and they look identical to a proximity query.
When to use this approach
Reach for structural repair only after the audit has produced a fragment list and the ordinary snapping pass has run. Repairing before snapping wastes effort on gaps the tolerance would have closed anyway, and it obscures whether the tolerance is set correctly.
The three causes have distinct signatures, which is what makes automatic classification possible:
- Clip-boundary severance shows a fragment whose edges terminate abruptly at the extract’s bounding geometry, usually with a
bridgeortunneltag on the terminating way. - Unmapped ferry access shows a fragment containing
route=ferryor a terminal amenity with no routable way reaching it. - Level separation shows two fragments that pass within metres of each other with
layerorbridgetags that differ — which is a correct representation, not damage.
Implementation
Classification first, driven by tags rather than by distance.
# requires: networkx, shapely (pip install networkx shapely)
import networkx as nx
from shapely.geometry import Point, box
def classify_fragment(G: nx.DiGraph, comp: set, extract_bounds) -> str:
"""Name the structural cause of an isolated component."""
edge_tags = [d for _, _, d in G.edges(comp, data=True)]
if any(d.get("route") == "ferry" for d in edge_tags):
return "ferry"
boundary = box(*extract_bounds).boundary
touches_edge = any(
boundary.distance(Point(G.nodes[n]["x"], G.nodes[n]["y"])) < 50.0
for n in comp
)
if touches_edge and any(
d.get("tunnel") or d.get("bridge") for d in edge_tags
):
return "clipped_structure"
layers = {d.get("layer") for d in edge_tags if d.get("layer")}
if layers and layers != {"0"}:
return "level_separated"
return "unclassified"
unclassified is a deliberate outcome rather than a failure. Fragments that fall through are the ones a human should look at, and they are usually genuine data errors worth reporting upstream to OSM.
Ferry connectors are the one case where a synthetic edge is defensible, and it needs recording:
# requires: networkx, scipy (pip install networkx scipy)
import networkx as nx
from scipy.spatial import cKDTree
import numpy as np
def add_ferry_connector(G: nx.DiGraph, terminal_node: int, road_nodes: list[int],
*, max_m: float = 1500.0, speed_kmh: float = 20.0) -> bool:
"""Link an isolated ferry terminal to the nearest routable road node."""
coords = np.array([(G.nodes[n]["x"], G.nodes[n]["y"]) for n in road_nodes])
tree = cKDTree(coords)
tx, ty = G.nodes[terminal_node]["x"], G.nodes[terminal_node]["y"]
dist, idx = tree.query([tx, ty])
if dist > max_m:
return False # too far to be a real access road
target = road_nodes[int(idx)]
seconds = int(dist / (speed_kmh / 3.6))
for u, v in ((terminal_node, target), (target, terminal_node)):
G.add_edge(
u, v,
length_m=float(dist),
travel_time_s=seconds,
highway="service",
synthetic="ferry_connector", # the flag that makes this reversible
)
return True
The synthetic attribute is the important line. It lets the connector be counted, reported, excluded from analyses that should only see real infrastructure, and removed once the corresponding OSM edit lands. A synthetic edge indistinguishable from a real one is technical debt that compounds every rebuild.
Clipped structures are not repaired in the graph at all — they are repaired upstream by re-extracting:
# Re-clip with complete_ways so a tunnel crossing the boundary arrives whole
osmium extract \
--polygon service-area.geojson \
--strategy complete_ways \
planet.osm.pbf --output region.osm.pbf --overwrite
Attempting to stitch a clipped tunnel inside the graph means inventing geometry for a way you do not have, and the invented geometry will be wrong in exactly the way that matters — its length, and therefore its cost.
Key parameters and tuning
| Parameter | Recommended value | Notes |
|---|---|---|
| Boundary proximity | 50 m | Distance from the extract edge that flags a clipped structure |
| Ferry connector max | 1 000–2 000 m | Beyond this the terminal is probably genuinely unreachable |
| Connector speed | 15–25 km/h | Terminal access roads are slow; a road-class default overstates them |
synthetic attribute |
always set | Without it, repairs become indistinguishable from data |
| Level-separated action | none | Joining them invents a movement no vehicle can make |
| Unclassified action | report | These are usually real OSM errors worth an upstream edit |
| Re-extract strategy | complete_ways |
The only real fix for boundary severance |
Integration points
From the audit. Fragment lists arrive from auditing connectivity after every graph rebuild with node counts and road names. Classification runs over that list rather than over the whole graph, which keeps it cheap.
Into ferry modelling. A repaired terminal connection is a prerequisite for the crossing edges built in layering ferry and rail edges onto road graphs. Without the connector the crossing exists but nothing can reach it, which is a fragment of exactly the shape this page repairs.
Back into OSM. Unclassified fragments and missing ferry access roads are genuine mapping gaps. Filing them upstream is the only fix that survives the next rebuild, and it removes a synthetic edge from your graph permanently.
Validation checklist
- Every synthetic edge is flagged. Count edges carrying the
syntheticattribute and confirm the number matches the repairs applied. An unflagged one is indistinguishable from real data forever. - Level-separated fragments are untouched. Assert that no synthetic edge joins two ways whose
layertags differ. That is the one repair that is always wrong. - Re-extraction actually resolved the clipped fragments. Rerun the audit after changing the extract strategy and confirm the clipped-structure count falls to zero rather than merely shrinking.
- Connector lengths are plausible. A ferry connector longer than a kilometre or two is suspicious; check whether the terminal snapped to the wrong road.
- Repairs survive a rebuild. Confirm the repair step runs on every build. A one-off manual fix vanishes silently at the next rebuild, and the audit will report the same fragment as new.
Related
- Graph fragmentation prevention in OSM data — snapping tolerance and the metric gaps it does close
- Auditing connectivity after every graph rebuild — producing the fragment list this repair pass consumes
- Layering ferry and rail edges onto road graphs — the crossing edges a repaired terminal makes reachable
- OSM data freshness and incremental updates — how an upstream OSM edit reaches the graph so a synthetic edge can be withdrawn