Skip to content

osm cockpit: draw the basemap from the bake, not from a tile CDN - #117

Merged
AdaWorldAPI merged 1 commit into
mainfrom
claude/q2-osm-map-reencoding-56p5e2
Aug 11, 2026
Merged

osm cockpit: draw the basemap from the bake, not from a tile CDN#117
AdaWorldAPI merged 1 commit into
mainfrom
claude/q2-osm-map-reencoding-56p5e2

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

Closes gap #1 from #116 ("the target is an area-fill base layer"). The map was someone else's raster with our data sprinkled on top — OSM_TILE_URLtile.openstreetmap.org, SAT_TILE_URL → Esri — and the 1.42 GB Berlin bake supplied only the feature dots plus the shape of the one feature you clicked. "The map pyramid and the slab's row key are one and the same address" was true as arithmetic while every pixel was rented; a deployed host on the OSMF tile server also runs against its usage policy.

The decode half already existed (.chains holds every way's z=32 vertex chain). This serves them per tile and draws them client-side in the SVG layer #116 added.

Measured, before → after

Headless Chromium against the shipped page, bake endpoints stubbed (this container has no slab). Identical harness run against main for the baseline, so the numbers are two-sided rather than self-reported:

before (main) after
phone map box 70×844 — 18% of a 390px viewport 390×490 — 100%
external requests 141–277 to tile.openstreetmap.org 0
touch pan does not move the map moves the map
raster <img> 18–42 0
shapes drawn 0 50–84 (from our endpoint)

Server

  • ShapeClass + class_for_tags — the semantic category is derived from tags the bake already stored, and served per shape. Shipping every shape's tags would multiply the payload by the tag fan-out for data a viewer wants about one shape at a time (the argument FeatureOut::idx already makes). FeatureGeometryOut carries the same field, so the click path and the basemap share one classification in Rust — the JS classFor(tags) rule is deleted rather than duplicated. The client keeps only the style table.
  • simplify_cells — drops vertices landing on the same (sub)pixel, as an integer compare on the codec's own z32 grid: a tile is 256px, so a world pixel at zoom z is the cell shifted right by 32 - (z + 8). The display's resolution is the tolerance, so there is no constant to tune. First and last vertices always survive, so a ring that survives stays closed.
  • query_tile_geometry — reuses overview_sample, so the cascade-cell selection that keeps isolated features applies here too. Reports total/sampled/returned/malformed separately: a thin basemap must read as LOD rather than missing data, and a corrupt sidecar must not masquerade as an empty neighbourhood.
  • GET /api/osm/geometry/tile/:z/:x/:y, registered after the :idx form.

Client

  • vector is the default basemap; the raster skins stay behind the toggle as the reference to check our render against, now an explicit opt-in.
  • The layer is retained across pan frames and rebuilt only on zoom — a pan changes the transform, not world-pixel coordinates, so rebuilding per frame would be the same n² append the drawnCells comment warns about.
  • The tile-source readout names the source actually in use (it printed tile.openstreetmap.org while nothing was being fetched from it — the exact confusion this view had to stop causing).

Mobile (bundled per request)

grid-template-columns:1fr 320px left the map 70px wide on a 390px viewport, and drag was bound to mousedown/mousemove with no touch-action, so the map could not be panned by finger at all. Stacked layout under a 720px width breakpoint (width, not device class — a narrow desktop window has the same problem), pointer events with a tracked pointerId so a second finger can't fight the first, and touch-action:none.

Tests

95 passed, 0 failed. The five new ones are two-sided by construction: class precedence is asserted in both tag orders (a first-match implementation returns Green for one of them), and simplification is pinned at z4/z22/z23 so it tracks pixel_shift's real arithmetic rather than a vague "fewer points when zoomed out". One of them failed first and was my expectation that was wrong — z22 has a 1-bit shift, so 201 of 400 is correct and full precision starts at z23.

Not verified

The render path over real Berlin data is unverified. This container lost the slab on restart and AWS_S3_BUCKET_NAME is unset, so ensure_slab_local cannot hydrate; every measurement above uses synthetic shapes through the real client code. Worth watching on the deploy: whether the overview zoom looks too sparse, since the budget is spent on rows and rows that are nodes carry no chain. If it does, the next rung is recorded in the plan (over-sample, then re-spread the chain-bearing survivors through overview_sample a second time).

Plan: claude-notes/plans/2026-08-11-osm-vector-basemap.md

🤖 Generated with Claude Code

https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw


Generated by Claude Code

The map was someone else's raster with our data sprinkled on top:
OSM_TILE_URL pointed at tile.openstreetmap.org and SAT_TILE_URL at Esri,
and the 1.42 GB Berlin bake supplied only the feature dots plus the shape
of the one feature you clicked. "The map pyramid and the slab's row key
are one and the same address" was true as arithmetic while every pixel
was rented — and a deployed host on the OSMF tile server also runs
against its usage policy.

The decode half already existed (.chains holds every way's z=32 vertex
chain; query_geometry reads one by row index). This serves them per
tile and draws them client-side in the SVG layer #116 added.

Server:
- ShapeClass + class_for_tags: the semantic category is derived from the
  tags the bake already stored and served per shape. Shipping every
  shape's tags would multiply the payload by the tag fan-out for data a
  viewer wants about one shape at a time. FeatureGeometryOut carries the
  same field, so the click path and the basemap share ONE classification
  in Rust — the JS classFor(tags) rule is deleted, not duplicated.
- simplify_cells: drops vertices landing on the same (sub)pixel, as an
  integer compare on the codec's own z32 grid — the display's resolution
  IS the tolerance, so there is no constant to tune. First/last always
  survive, so a ring that survives stays closed.
- geometry_row_budget + query_tile_geometry, reusing overview_sample so
  the cascade-cell selection keeps isolated features. Reports
  total/sampled/returned/malformed separately: a thin basemap must read
  as LOD, and a corrupt sidecar must not look like an empty
  neighbourhood.

Client:
- vector is the DEFAULT basemap; the raster skins stay behind the toggle
  as the reference to check our render against.
- The layer is retained across pan frames and rebuilt only on zoom. A
  pan changes the transform, not world-pixel coordinates, so rebuilding
  per frame would be the same n^2 append the drawnCells comment warns
  about.
- The tile-source readout names the source actually in use.

Mobile (bundled per operator request): the page was unusable on a phone.
grid-template-columns:1fr 320px left the map 70px wide on a 390px
viewport, and drag was bound to mousedown/mousemove with no touch-action,
so the map could not be panned by finger at all. Stacked layout under a
720px breakpoint, pointer events with a tracked pointerId, and
touch-action:none.

Measured before/after in headless Chromium against the shipped page, with
the bake endpoints stubbed (the container has no slab):

                  before                    after
  phone map       70x844  (18% of vp)       390x490 (100%)
  external req    141-277 to tile.osm.org   0
  touch pan       does not move the map     moves the map
  raster <img>    18-42                     0

Tests: 95 passed, 0 failed. The five new ones are two-sided —
class precedence is checked in both tag orders (a first-match
implementation returns Green for one of them), and simplification is
pinned at z4/z22/z23 so it tracks pixel_shift's real arithmetic rather
than "fewer points when zoomed out".

NOT verified against the real bake: this container lost the slab on
restart and AWS_S3_BUCKET_NAME is unset, so the render path over real
Berlin data is unverified until the deploy.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

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: 964f7654-945a-41fd-89af-af2435fc2605

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.

@cursor

cursor Bot commented Aug 11, 2026

Copy link
Copy Markdown

Bugbot couldn't run - usage limit reached

Bugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit.

A user or team admin can review and increase usage limits in the Cursor dashboard.

(requestId: serverGenReqId_0b9dc9bc-cf31-40c6-961f-9e4912c8cbdb)

@AdaWorldAPI
AdaWorldAPI marked this pull request as ready for review August 11, 2026 20:29
@AdaWorldAPI
AdaWorldAPI merged commit a644490 into main Aug 11, 2026
5 checks passed
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.

2 participants