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 bridge or tunnel tag on the terminating way.
  • Unmapped ferry access shows a fragment containing route=ferry or a terminal amenity with no routable way reaching it.
  • Level separation shows two fragments that pass within metres of each other with layer or bridge tags that differ — which is a correct representation, not damage.
Three shapes that look the same to a proximity query A clipped tunnel ends at the extract boundary and needs re-extraction. A ferry terminal sits a kilometre from the nearest road with no mapped link and needs a connector. A footbridge crosses a motorway on a different layer and needs nothing — joining them would invent an impossible movement. Classify before repairing — one of these must be left alone clip-boundary severance extract edge tunnel way cut in half fix: re-extract complete_ways unmapped ferry access coast road terminal, 900 m off fix: add a connector edge level separation motorway, layer 0 footbridge, layer 1 fix: none — this is correct A proximity query sees three pairs of nearby fragments. Only the tags distinguish the two that need repair from the one that does not.

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.

Tracking repairs across builds Across six builds the clipped-structure count drops to zero once the extract strategy is corrected, while synthetic ferry connectors decline gradually as upstream OSM edits land and each connector is withdrawn. Fragments by cause, across six builds 18 0 b1 b3 b6 clipped structures — fixed by re-extracting at b3 synthetic ferry connectors — withdrawn as OSM edits land A synthetic-connector count that never falls means nobody is filing the upstream edits, and the graph is accumulating permanent local divergence. Tracking it per build turns that from an invisible drift into a number somebody owns. How a national build’s fragments classify Of 253 reported fragments in a national build, 38 are clipped structures fixed by re-extraction, 11 are unmapped ferry access needing a synthetic connector, and 204 are genuine level separations that require no action at all. Fragment classification outcomes across one national build clipped structure 38 fragments fix upstream by re-extracting unmapped ferry access 11 fragments synthetic connector, flagged level separated 204 fragments correct — leave alone The large majority need nothing done. Classifying first is what stops a repair pass welding two hundred correctly separated crossings together. Track the eleven synthetic connectors by name — each one is a pending OSM edit, and the count should fall over time rather than accumulate.

Validation checklist

  • Every synthetic edge is flagged. Count edges carrying the synthetic attribute 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 layer tags 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.