Skip to content

Harden merge workflows against routing regressions - #218

Open
trevorgerhardt wants to merge 22 commits into
mainfrom
driving-after-merge
Open

Harden merge workflows against routing regressions#218
trevorgerhardt wants to merge 22 commits into
mainfrom
driving-after-merge

Conversation

@trevorgerhardt

@trevorgerhardt trevorgerhardt commented Jul 21, 2026

Copy link
Copy Markdown
Member

Summary

This PR makes OSM merges safer and adds routing tests for merged data.

The exact merge is now conservative. It compares patch entities only with the original base. An empty patch does not change the base.

The PR also restores the original 1-meter match for imported data. This match is now an optional and visible workflow step.

Users can transfer selected tags, attach imported networks, or do both. Unsafe or unclear matches require review or remain blocked.

What we found

The Monaco fixture reproduced the reported routing error. Before this fix, an empty merge changed the dataset:

  • It removed 247 nodes.
  • It changed 232 ways.
  • It reduced 30 highways to one node.
  • It changed route distances and reachability.

Two Monaco routes show the effect:

  • 1866510534 → 937988247 changed from approximately 1,043 m to an invalid 29 m shortcut.
  • 1875118274 → 12281555152 changed from approximately 164 m to unreachable.

The old merge matched nearby entities inside each input. Distance alone cannot prove that two OSM entities represent the same feature.

Nearby nodes can represent tunnels, bridges, barriers, parallel roads, or different network levels. The old replacement changed way and relation references.

The routing review also found three separate router errors:

  • BinaryHeap.bubbleDown sometimes put items in the wrong priority order.
  • The router did not apply the direction from junction=roundabout.
  • The router treated oneway=-1 as two-way travel.

The Eastern Washington fixtures found an intersection error. A valid full merge collapsed patch ways after several intersection nodes mapped to one base node.

The fix groups intersections by segment and inserts them in geometric order. It rejects an insertion that creates repeated references or a degenerate way.

Safe exact merge

The merge no longer does proximity-based cleanup inside the base or patch. Exact reconciliation has these rules:

  • It compares new patch entities only with the original base.
  • It requires one compatible target at OSM coordinate precision.
  • It preserves the base ID.
  • It does not create replacement chains.
  • It keeps same-ID patch updates authoritative.
  • It rejects conflicts in routing, access, barrier, layer, level, bridge, tunnel, or covered context.
  • It rejects changes that collapse a routable way.
  • It matches ways only when node order and routing meaning agree.

Intersection creation now inserts all nodes in segment order. It connects only crossings with compatible grade context.

The merge rejects a changeset that creates one of these results:

  • A dangling way or relation reference.
  • A highway with fewer than two distinct nodes.
  • A broken via-node or via-way restriction.
  • A new connection between grade-separated highways.

The error includes the related OSM entity IDs. The merge does not silently remove or repair restrictions.

Optional imported-data matching

Imported GeoJSON, Shapefile, and OSW data often use different IDs and slightly different coordinates. These files still need proximity matching.

The new workflow searches from the untouched patch into the original base. It does not compare entities within one input.

Imported-data matching is off by default. The recommended configuration uses these values:

  • A candidate radius of 1 meter.
  • Automatic use of high-confidence matches.
  • Property keys barrier, crossing, kerb, and tactile_paving.
  • Network attachment off until the user selects it.

Property transfer and network attachment are separate actions:

Action Change Data that stays authoritative
Transfer properties Copies present patch values for selected keys Base ID, coordinates, references, and relation members
Attach network Rewrites references in patch-created ways to a base node All existing base way references and relation members

An absent patch value does not remove a base value. The merge does not copy an unselected key.

Protected structural tags cannot transfer through a fuzzy match. Routing tags require review.

Automatic node attachment requires one compatible target and a one-to-one mapping. It also requires compatible routing, access, grade, and segment direction.

The merge blocks grade conflicts, restrictions, dangling references, duplicate adjacent references, and collapsed ways. It rejects area-only school and building vertices as routing targets.

This PR supports nodes and one source way to one base way. It reports one-to-many way chains as unsupported-way-chain.

Candidate review and recovery

The Merge app has a Match imported data step. It shows match evidence, tag differences, route context, and clear status labels.

The app supports filter-wide actions across all result pages:

  • Transfer properties (N) applies the property decision to eligible filtered matches.
  • Attach network (N) applies the attachment decision to eligible filtered matches.
  • Reject filtered (N) rejects all filtered matches that are not already rejected.

The confirmation shows changed, skipped, automatic, review, and replaced-decision counts. Blocked and ambiguous candidates cannot use a bulk accept action.

When a filter changes, the old list remains visible as stale data. A spinner shows that the worker is updating the results.

The worker stores discovery options and decisions for restart recovery. A new input invalidates decisions from the previous input.

Merge app changes

The app now explains every workflow step. Collapsed details describe inputs, changes, safety rules, and outputs.

Other workflow changes include:

  • Inline help uses accessible info tooltips.
  • Automatic merge shows completed, current, and remaining steps.
  • The current worker message appears below the current step.
  • Users can load the base OSM on the Merge page.
  • Base and patch cards show filenames, Download, and Clear actions.
  • Diagnostic duplicate scans have a Skip action.
  • Step actions use a vertical layout that works in narrow sidebars.
  • Candidate results become inert while a filter update is pending.

Diagnostic scans do not change OSM data. Reviewed and automatic workflows use the same merge rules.

Routing and merge tests

The routing harness records stable OSM evidence. It compares Dijkstra and A* without using internal graph indexes as expected values.

The Monaco matrix covers car and walk routes, one-way roads, roundabouts, steps, access cases, restrictions, and unreachable points.

Each accepted route runs against raw, merged, and PBF-reloaded data. Synthetic fixtures cover intersections, grade separation, restrictions, and imported-network attachment.

Local R5 runs remain the authority for Conveyal route legality. R5 is not a package or CI dependency.

The raw and merged Monaco matrices matched in local R5 at commit ac95649c7094bf394b3be43fa523d0fb4447633e.

The Yakima test used the 1-meter configuration with property transfer and network attachment:

Status Count
Total 11,689
Automatic 145
Review 212
Blocked 88
Unmatched 11,244

These results include valid sidewalk matches and protected school-boundary cases. The large Yakima files remain local fixtures and do not run in CI.

Test speed and CI reliability

The Merge browser tests now use three ordered Playwright projects. One Chromium worker runs the real Merge flow, guidance tests, and worker-runtime tests.

The real Merge flow loads each input once. Lightweight tests cover responsive layout, filenames, file actions, and guidance text.

The real test uses the tracked fixtures/monaco.pbf file. The prior CI test requested an ignored fixture and received Vite HTML instead of PBF data.

The load helper now reports worker alerts immediately. GeoParquet tests build the Monaco fixture once for the suite.

The final hosted CI run passed without test.slow(). The browser suite completed 16 tests in 21.8 seconds.

The final local run completed 16 browser tests in 26.3 seconds. The original local baseline was 46.7 seconds.

Compatibility

  • Exact merge remains the default.
  • Fuzzy matching requires an explicit conflation configuration.
  • Existing merge and worker method signatures remain compatible.
  • deduplicateNodes and deduplicateWays now mean safe patch-to-base reconciliation.
  • Same-ID patch updates remain authoritative.
  • PBF serialization is unchanged.
  • Full relation conflation and one-to-many way matching remain outside this PR.

Old merged PBF files can contain damaged references. Regenerate these files from the original base and patch inputs.

Verification

  • pnpm run verify:workspace -- @osmix/change
  • pnpm run verify:workspace -- @osmix/router
  • pnpm run verify:workspace -- @osmix/geoparquet --write
  • pnpm run verify:workspace -- osmix
  • pnpm run verify:workspace -- apps/merge
  • pnpm run check:deps
  • pnpm run test — 834 passed and 3 skipped
  • Merge unit tests — 61 passed
  • Merge browser tests — 16 passed in 21.8 seconds on hosted CI
  • git diff --check
  • osmium check-refs — no missing references in tested PBF files

@vercel

vercel Bot commented Jul 21, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
osmix Ready Ready Preview Aug 11, 2026 9:02am
osmix-merge Ready Ready Preview Aug 11, 2026 9:02am

Request Review

@changeset-bot

changeset-bot Bot commented Jul 21, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: c7deefb

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 4 packages
Name Type
@osmix/change Patch
@osmix/router Patch
osmix Patch
@osmix/cli Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

Why:
The worker-backed Merge browser journey repeated PBF loads and MapLibre layout checks while worker restart tests competed for the same CI runner, causing a 120-second timeout.

Decisions:
Keep one real Monaco load per input, exercise presentation behavior through the production input header in the lightweight harness, and run worker-runtime tests after Merge UI tests.

Changes:
Extract the loaded input-card header, move clear/download/overflow checks to the harness, consolidate responsive cases, and document the browser-test boundary.

Validation:
pnpm run verify:workspace -- apps/merge --write
CI=true pnpm --filter @osmix/merge test:e2e
pnpm run check:deps
pnpm run test
git diff --check
Why:
Hosted CI still hung while the Merge journey streamed its second fixture through Vite, and the GeoParquet suite rebuilt the same fixture often enough to exceed its normal test timeout under load.

Decisions:
Exercise the real local-file selection workflow with a single app worker, keep multi-worker behavior in the dedicated runtime project, and share immutable GeoParquet fixture setup across assertions.

Changes:
Select checked-in Monaco PBFs with Playwright file choosers, fail immediately on load-error panels, assert the test worker count, and cache the GeoParquet fixture and converted OSM in beforeAll.

Validation:
CI=true pnpm --filter @osmix/merge test:e2e
pnpm run verify:workspace -- @osmix/geoparquet --write
pnpm run check:deps
pnpm run test
git diff --check
Why:
Hosted Chromium can finish navigation before the top-level worker module exposes window.osmWorker, so the worker-count assertion must tolerate initialization.

Changes:
Return zero until the remote exists and let the existing Playwright poll wait for the expected single worker.

Validation:
CI=true pnpm --filter @osmix/merge test:e2e
pnpm --filter @osmix/merge typecheck
Why:
The local-only monaco.test.pbf is ignored by Git, so hosted Playwright could not select it; the earlier URL-based test had silently received Vite HTML instead and hung in PBF parsing.

Changes:
Use the tracked Monaco PBF for both Merge roles while the lightweight harness retains distinct filename coverage.

Validation:
CI=true pnpm --filter @osmix/merge test:e2e (16 passed in 23.2s)
git diff --check
Why:
The two-core hosted runner still starved a lightweight guidance page while it ran alongside the real Merge journey, even after the PBF fixture issue was fixed.

Decisions:
Run the real Merge integration, guidance harness, and worker-runtime coverage as ordered Playwright projects so only one Chromium worker is active at a time.

Changes:
Split Merge integration and guidance into separate projects and make worker-runtime depend on guidance.

Validation:
CI=true pnpm --filter @osmix/merge test:e2e (16 passed in 26.3s)
pnpm --filter @osmix/merge typecheck
pnpm exec oxlint --type-aware apps/merge
git diff --check
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant