Skip to content

fix(osm): index every part of a multipolygon barrier - #263

Open
cs-util wants to merge 1 commit into
r466from
r467
Open

fix(osm): index every part of a multipolygon barrier#263
cs-util wants to merge 1 commit into
r466from
r467

Conversation

@cs-util

@cs-util cs-util commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

Acts on the review findings from #260.

  • fix(osm): index every part of a multipolygon barrier

Review on #260 found that polygons[0][0] drops more than holes. The inner
index correctly ignores holes; the outer one silently discarded
polygons[1..], which are disjoint PARTS of the same barrier. One part
indexed and the other invisible is precisely the 'a barrier the index
simply did not see' failure the multipolygon branch was added to remove,
moved one level in.

barrierLine becomes barrierLines and returns every outer ring, sharing
one frame across the feature so all parts are expressed against the same
anchor. A relation with two stitched outer rings covers it; reverting to
first-part-only fails that test.

The multilinestring branch is dropped as unreachable: toGeometry never
produces one — only clip.ts does, and clipping is not in this path — so
it was code no test could ever cover.

The cell-coverage comment claimed to remove polygonToCells calls and did
not. coverCells still runs once per ring, and batching cannot change that
since coverCells runs addPolygon per ring internally. What went away is
the existing.includes rescan, and the union making one-obstacle-per-cell
structural. Narrowed to what it actually does — in a PR family whose
subject is comments describing behaviour that does not exist.
@coderabbitai

coderabbitai Bot commented Aug 6, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 370c0bc9-22e0-4c9e-91ac-20a666c3b085

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@claude

claude Bot commented Aug 6, 2026

Copy link
Copy Markdown

Claude finished @cs-util's task in 3m 6s —— View job


Review

  • Read .github/claude-review-guidelines.md
  • Analyze the diff vs origin/r466
  • Verify the multilinestring claim and the multipolygon path
  • Check test coverage & sidecar docs
  • Post review

The core fix is correct. barrierLines now returns every outer ring, buildObstacleIndex flatMaps the footprints, and the new test asserts both parts land in distinct cells. I verified the two load-bearing claims:

  • toGeometry really never produces multilinestring — it emits point/linestring/polygon/multipolygon only (osm-geometry.ts:180-256); the kind is produced solely by clip.ts:273. Deleting the branch is safe.
  • One ENU frame for the whole feature is fine even for parts kilometres apart. toEnu/toLatLng are exact inverses (enu.ts), so vertex positions round-trip unchanged; only the thickness offset carries the frame's cos(anchorLat) scale, and enu.ts puts that under 0.05 % over 3 km — far below OSM footprint accuracy.

Four things raised inline, none blocking:

  1. obstacles.ts:104-111 — both ?? [] fallbacks are unreachable by construction (wayToGeometry builds rings: [way.geometry]; groupRingsIntoPolygons seeds every polygon as [outer]). That's the same "a branch no test could ever cover" category this PR deletes multilinestring for. Suggestion included; widening the annotation to readonly (readonly LatLng[])[] also drops three full-vertex array copies.
  2. obstacles.test.ts:154 — the one-outer-ring relation is still untested, and it's the more common shape: relationToGeometry returns kind: "polygon" for a single stitched outer and only falls to multipolygon for ≥2 disjoint outers. That polygon branch was also rewritten here and no fixture reaches it — closed barrier=wall ways don't, since POLYGON_FEATURES blacklists barrier=wall so isAreaWay sends them to linestring.
  3. obstacles.ts:98-99 — "a barrier's holes are not something to walk through" doesn't match the code: barrierFootprints treats each ring as a centreline, so only a ~0.5 m band along it is ever solid and the interior is walkable either way. For an area-mapped barrier=city_wall (outer face / inner face), the wall material is the band between the rings. Pre-existing behaviour, but the rewritten comment is a good moment to state it accurately — same for the new obstacles.ts.md bullet.
  4. obstacles.ts:169coverCells calls addPolygon per polygon, not per ring. Conclusion unaffected, but this comment replaces one that was wrong about the same function.

I could not run pnpm test / vitest in this environment (command not permitted), so the new test is reviewed by reading only — the fixture geometry checks out (two closed triangles ~444 m apart, each ~18 m across, so groupRingsIntoPolygons yields two polygons and the parts land in different res-13 cells).

No changes pushed — review only.
· branch r467

@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
gps-plus-slam 0474775 Commit Preview URL

Branch Preview URL
Aug 06 2026, 02:53 AM

Comment on lines +104 to +111
const lines: readonly LatLng[][] =
geometry.kind === "linestring"
? geometry.positions
? [[...geometry.positions]]
: geometry.kind === "polygon"
? geometry.rings[0]
? [[...(geometry.rings[0] ?? [])]]
: geometry.kind === "multipolygon"
? geometry.polygons[0]?.[0]
: geometry.kind === "multilinestring"
? geometry.lines[0]
: undefined;
if (positions === undefined || positions.length < 2) return undefined;
? geometry.polygons.map((polygon) => [...(polygon[0] ?? [])])
: [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both ?? [] fallbacks are unreachable, which is the same category the comment three lines above deletes multilinestring for.

  • kind === "polygon"wayToGeometry builds rings: [way.geometry] (osm-geometry.ts:203) and relationToGeometry returns polygons[0]! (osm-geometry.ts:253). rings[0] is always there.
  • kind === "multipolygon"groupRingsIntoPolygons seeds every entry as [outer] (multipolygon-builder.ts:379), so polygon[0] is always there.

So ?? [] is a branch no test can cover and no mutant can be killed on — exactly the argument made for leaving multilinestring out. ! is what the rest of this file already uses for the same situation (line 144: lines[0]![0]!.y).

Suggested change
const lines: readonly LatLng[][] =
geometry.kind === "linestring"
? geometry.positions
? [[...geometry.positions]]
: geometry.kind === "polygon"
? geometry.rings[0]
? [[...(geometry.rings[0] ?? [])]]
: geometry.kind === "multipolygon"
? geometry.polygons[0]?.[0]
: geometry.kind === "multilinestring"
? geometry.lines[0]
: undefined;
if (positions === undefined || positions.length < 2) return undefined;
? geometry.polygons.map((polygon) => [...(polygon[0] ?? [])])
: [];
const lines: readonly (readonly LatLng[])[] =
geometry.kind === "linestring"
? [geometry.positions]
: geometry.kind === "polygon"
? [geometry.rings[0]!]
: geometry.kind === "multipolygon"
? geometry.polygons.map((polygon) => polygon[0]!)
: [];

Widening the annotation to readonly (readonly LatLng[])[] also drops the three spreads — those copy every vertex of every ring purely to satisfy mutability variance, and nothing here mutates them.

Comment on lines +154 to +156
it("indexes EVERY part of a multipolygon barrier, not just the first", () => {
// RAISED IN REVIEW ON #260. The multipolygon branch took `polygons[0][0]`,
// where the inner index correctly ignores holes but the OUTER one silently

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This pins the two-part case, but the one-part relation is still untested — and it is the more common shape of the two.

relationToGeometry returns kind: "polygon" when the outer rings stitch to a single ring, and only falls through to kind: "multipolygon" for ≥2 disjoint outers (osm-geometry.ts:252-255). So an ordinary type=multipolygon + barrier=wall relation with one outer ring lands on the polygon branch, which this PR also rewrote (geometry.rings[0][[...(geometry.rings[0] ?? [])]]) and which no fixture in this file reaches — closed barrier=wall ways don't get there either, since osmtogeojson blacklists barrier=wall in POLYGON_FEATURES, so isAreaWay sends them to linestring.

Given the mutation-testing bar this file already holds itself to (the "EVERY segment of a bent barrier" test above), a single-outer-ring relation fixture would be a cheap addition next to the one you just wrote — same relation shape, one outer member.

Comment on lines +98 to +99
// OUTER RINGS ONLY, and ALL of them: a barrier's holes are not something to
// walk through, but its parts are all wall.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "all outer rings" half is right; the hole rationale doesn't describe what the code does.

"a barrier's holes are not something to walk through" only makes sense if the polygon's interior were being treated as solid. It isn't: barrierFootprints treats the ring as a centreline and emits one thicknessM-wide quad per segment (barrier-shape.ts), so all that ever becomes solid is a ~0.5 m band along the ring itself. The interior is fully walkable whether or not the inner rings are read, so dropping them isn't what keeps holes closed.

Where it actually matters: an area-mapped barrier=city_wall is normally outer = outer face, inner = inner face, and the wall material is the band between them. This code puts a default-thickness band on the outer face and ignores the inner one. That behaviour predates this PR and is defensible, but since you're rewriting this comment anyway, saying what it does — "each outer ring is treated as a centreline; an area-mapped barrier is indexed as a wall along its outline, not as a filled region" — would be more durable than the current claim. Same for the sidecar bullet in obstacles.ts.md.

// WHAT THIS REMOVES IS THE RESCAN, not the h3 calls — an earlier comment
// here claimed the latter and was wrong (#260). `coverCells` still runs
// once per ring, and batching cannot change that: `coverCells` on a
// multipolygon runs `addPolygon` per ring internally, so the per-quad cost

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: coverCells runs addPolygon once per polygon (outer + its holes), not once per ring — cell-coverage.ts:75-77. The conclusion holds, since in the batched alternative you're arguing against each quad would be its own polygon, but this comment is replacing one that was wrong about this same function, so it's worth being literal: "runs addPolygon per polygon, and each quad would be its own polygon".

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