Skip to content

cockpit-server: OSM tile material — WebMercator z/x/y ↔ HHTL + tile source#73

Merged
AdaWorldAPI merged 2 commits into
mainfrom
claude/q2-osm-tile-material
Jul 5, 2026
Merged

cockpit-server: OSM tile material — WebMercator z/x/y ↔ HHTL + tile source#73
AdaWorldAPI merged 2 commits into
mainfrom
claude/q2-osm-tile-material

Conversation

@AdaWorldAPI

@AdaWorldAPI AdaWorldAPI commented Jul 5, 2026

Copy link
Copy Markdown
Owner

What

The Geo-domain (0x0F) map material for the /OSM cockpit — where OSM
maps come from
, and how a slippy-tile address becomes an HHTL key. A new
self-contained cockpit-server::osm_tiles module.

The two halves (per MERCATOR-HHTL-HELIX-MAP.md)

  1. The source. OSM_TILE_URL = the standard slippy-tile server
    (tile.openstreetmap.org/{z}/{x}/{y}.png). The client fetches the raster;
    the cockpit only computes the address — no network on the request path.
  2. The address. z/x/y is the WebMercator quadtree; a quadtree is a
    cascade, so it Morton-interleaves into HHTL's three 16-bit tiers
    (3 × 256×256 = 48 bits). Coarse zooms left-align into HEEL
    (tier = level >> 3), fine into TWIG — the map pyramid and the semantic
    cascade are one address (D-BOTHCASC).

Surface

function role
lonlat_to_tile WebMercator (EPSG:3857) forward, no PROJ
morton_interleave / morton_deinterleave 24-bit lanes ↔ 48-bit code
tile_to_hhtl z/x/y{heel, hip, twig}
tile_url the concrete source URL

Routes: GET /api/osm/locate?lon=&lat=&z= and GET /api/osm/tile/:z/:x/:y
tile address + source URL + HHTL key.

Tests (8)

null-island center tile, Berlin = (8802, 5373), east→+x / south→+y,
Morton round-trip, HHTL round-trip at native depth, coarse-zoom-lands-in-HEEL,
adjacent-tiles-share-HEEL.

Verification note (please read)

The tile math is verified standalone via rustc — all assertions pass
(Berlin=(8802,5373), hhtl(2,3,1)=(28672,0,0)). The full
cargo test -p cockpit-server cannot run in my sandbox: the crate's
pre-existing v8 v149.4.0 transitive dep fails its build.rs (needs
gn/ninja tooling not present here). This is orthogonal to this change — it adds
only std + serde + axum code and standard-pattern handlers. The unit
tests run under CI where the v8 toolchain is available. Flagging per q2's
"say so explicitly if you can't verify end-to-end" rule.

Next

The /OSM cockpit page consumes these endpoints — it's the stacked
follow-up PR.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP


Generated by Claude Code

Summary by CodeRabbit

  • New Features

    • Added two new map-related API endpoints to look up a location’s tile and retrieve tile metadata.
    • Responses now include the tile address, source URL, and a compact internal tile key for easier integration.
    • Support was added for resolving tile requests to the nearest native-resolution ancestor when needed.
  • Tests

    • Added coverage for tile lookup, coordinate conversion, URL generation, and key round-tripping behavior.

…ource

The Geo-domain (0x0F) map material for the /OSM cockpit: where OSM maps come
from, and how a slippy-tile address becomes an HHTL key.

- OSM_TILE_URL — the canonical slippy-tile source
  (tile.openstreetmap.org/{z}/{x}/{y}.png); the client fetches the raster, the
  cockpit only computes the address (no network on the request path).
- lonlat_to_tile — WebMercator (EPSG:3857) forward, no PROJ
  (asinh(tan φ) = ln(tan+sec)).
- morton_interleave / tile_to_hhtl — z/x/y quadtree → 48-bit Morton → the three
  16-bit HHTL tiers (HEEL/HIP/TWIG), coarse zoom left-aligned into HEEL
  (tier = level>>3). The map pyramid and the semantic cascade are ONE address
  (D-BOTHCASC), per docs/MERCATOR-HHTL-HELIX-MAP.md.
- Routes: GET /api/osm/locate?lon=&lat=&z= and /api/osm/tile/:z/:x/:y — return
  tile address + source URL + HHTL key.

8 unit tests (null-island center tile, Berlin=(8802,5373), east→+x/south→+y,
Morton round-trip, HHTL round-trip at native depth, coarse-zoom-in-HEEL,
adjacent-tiles-share-HEEL).

Verification note: the tile MATH is verified standalone via rustc (all
assertions pass). The full `cargo test -p cockpit-server` cannot run in this
sandbox — the crate's pre-existing `v8` transitive dep fails its build.rs
(needs gn/ninja tooling not present here); this is orthogonal to this change,
which adds only std+serde+axum code. The unit tests run under CI where the v8
toolchain is available.

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

coderabbitai Bot commented Jul 5, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a new osm_tiles module to the cockpit-server crate implementing WebMercator tile math, Morton-coded HHTL key derivation, and two HTTP GET handlers for locating tiles and fetching tile metadata, wired into the main Axum router with accompanying unit tests.

Changes

OSM Tile Locate and Metadata Feature

Layer / File(s) Summary
Core tile and HHTL math
crates/cockpit-server/src/osm_tiles.rs
Defines OSM_TILE_URL and HHTL_DEPTH constants, tile_url, lonlat_to_tile WebMercator projection, Morton interleave/deinterleave utilities, and the Hhtl struct with tile_to_hhtl/resolved_tile folding logic for zooms beyond native depth.
HTTP handlers and router wiring
crates/cockpit-server/src/osm_tiles.rs, crates/cockpit-server/src/main.rs
Adds LocateQuery, osm_locate_handler, and osm_tile_meta_handler returning JSON tile metadata; declares mod osm_tiles; and registers /api/osm/locate and /api/osm/tile/:z/:x/:y routes.
Unit tests
crates/cockpit-server/src/osm_tiles.rs
Adds tests for URL templating, tile projection correctness, axis direction, Morton roundtrips, HHTL roundtrips, tier locality, and over-depth folding.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant Router
  participant OsmTilesHandler
  participant TileMath

  Client->>Router: GET /api/osm/locate?lon&lat&z
  Router->>OsmTilesHandler: osm_locate_handler(LocateQuery)
  OsmTilesHandler->>TileMath: lonlat_to_tile(lon, lat, z)
  TileMath-->>OsmTilesHandler: (x, y)
  OsmTilesHandler->>TileMath: tile_to_hhtl(z, x, y)
  TileMath-->>OsmTilesHandler: Hhtl
  OsmTilesHandler-->>Client: JSON {tile_url, source, hhtl, geo_domain}

  Client->>Router: GET /api/osm/tile/:z/:x/:y
  Router->>OsmTilesHandler: osm_tile_meta_handler(Path)
  OsmTilesHandler->>TileMath: resolved_tile(z, x, y)
  TileMath-->>OsmTilesHandler: (z', x', y')
  OsmTilesHandler-->>Client: JSON tile metadata with resolved ancestor
Loading

Poem

A hop, a skip, through zoom and lane,
Morton bits interleave the plane. 🐇
From lon and lat to tile so small,
HHTL folds ancestors, one and all.
Two new roads for the router to run —
This bunny's tile-math work is done!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main change: OSM tile math, HHTL mapping, and tile source handling in cockpit-server.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch

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 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 742b540. Configure here.

heel: (code >> 32) as u16,
hip: (code >> 16) as u16,
twig: code as u16,
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Deep zoom HHTL uses wrong bits

Medium Severity

For slippy requests with z above HHTL_DEPTH, tile_to_hhtl only caps z to 24 and sets the left-align shift to zero, leaving x and y at the finer zoom scale. Morton encoding then uses the low 24 bits instead of the quadtree prefix, so /api/osm/tile/:z/:x/:y can return an hhtl key that does not match the requested tile.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 742b540. Configure here.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 742b540c77

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +100 to +102
let z = z.min(HHTL_DEPTH);
let shift = HHTL_DEPTH - z;
let code = morton_interleave(x << shift, y << shift);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize coordinates when clamping zoom

In the /api/osm/tile path, this clamps only z before encoding but leaves the over-depth x/y unchanged. For a valid slippy address with z > 24, the HHTL key should either be rejected or represent the z=24 parent tile (for example, z=25 x=2 has parent x=1), but this encodes x=2 and drops any bits above the 24-bit lane, so the returned hhtl no longer identifies the tile URL in the same response and high-zoom tiles can alias incorrectly.

Useful? React with 👍 / 👎.

Review fix (Bugbot Medium + codex P2 on #73): for z > 24, tile_to_hhtl only
capped z and set the left-align shift to 0, leaving x/y at the finer scale so
Morton encoded the LOW 24 bits — the HHTL key didn't match the requested tile
and deep tiles could alias.

Now a zoom deeper than the native depth resolves to its z=24 ANCESTOR: the
excess low bits of x/y are dropped (x >> (z-24)), so the key is always a valid
prefix of the tile and two children of one z=24 tile share it. New
`resolved_tile()` exposes the ancestor z/x/y, and /api/osm/tile/:z/:x/:y now
returns a `resolved` field so the key and address never silently describe
different tiles. Locate is unaffected (its z is already clamped ≤ 24).

Test `over_depth_zoom_folds_to_its_native_ancestor` locks it (verified
standalone via rustc: hhtl(25,2,3) == hhtl(24,1,1), ≠ the old low-bit value).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP
AdaWorldAPI pushed a commit that referenced this pull request Jul 5, 2026
The Geo-domain (0x0F) sibling of the FMA body-helix cockpit. A self-contained
`/osm` page (the /mri page pattern — one inline HTML string, no build step, no
external JS):

- pan/zoom OSM raster tiles fetched directly from the standard source
  (tile.openstreetmap.org), with attribution;
- click any point → GET /api/osm/locate → the tile's z/x/y + source URL +
  the three HHTL tiers (HEEL/HIP/TWIG) shown live;
- the JS slippy math mirrors cockpit-server::osm_tiles; the HHTL key is
  resolved server-side so the map pyramid and the cascade address are one
  source of truth.

Route: GET /osm.

Verification note: the embedded JS is validated via `node --check` (3.1 KB, OK)
and the Rust raw-string delimiter is confirmed collision-free. The full
`cargo test -p cockpit-server` cannot run in this sandbox (pre-existing `v8`
transitive dep fails its build.rs — no gn/ninja toolchain); the page handler is
a trivial `Html<String>` const mirroring the existing `mri_page_handler`, wired
like every other cockpit route. Stacks on #73 (the osm_tiles material it calls).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EYvNjD8M8LMNYbRy3gq2FP
AdaWorldAPI pushed a commit that referenced this pull request Jul 5, 2026
… on click

Review fixes (Bugbot + codex on #74):
- Pan-end `mouseup` fired a `click` that ran /api/osm/locate for the release
  point; `if(drag) return` never applied because `mouseup` nulls `drag` first.
  Track a `moved` flag (set on real drag) and suppress the trailing click.
- The zoom +/− controls live inside #map, so their clicks bubbled to the map's
  click→locate handler. `e.stopPropagation()` on both.
- The click path sent `x2lon(fx)` without wrapping `fx` into [0,2^z); at low
  zoom / across ±180° that yields an out-of-range longitude the server clamps
  to an edge tile. Wrap `fx` modulo 2^z before converting.

Embedded JS re-validated via `node --check`. (osm_tiles over-depth fix rides in
from #73, which this branch now rebases onto.)

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

Copy link
Copy Markdown
Owner Author

Review fix pushed (b14f387). Deep-zoom HHTL bug (Bugbot Medium + codex P2): tile_to_hhtl now folds an over-native-depth tile (z > 24) down to its z=24 ancestor — the excess low bits of x/y are dropped (x >> (z-24)) instead of encoded into the low Morton lane. So the key is always a valid prefix of the requested tile, two children of one z=24 tile share it, and /api/osm/tile/:z/:x/:y now returns a resolved field naming the ancestor so the key and address never silently describe different tiles. locate was already clamped ≤ 24. New test over_depth_zoom_folds_to_its_native_ancestor (verified standalone via rustc: hhtl(25,2,3) == hhtl(24,1,1), ≠ the old low-bit value).


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
crates/cockpit-server/src/osm_tiles.rs (1)

25-28: 🧹 Nitpick | 🔵 Trivial

Confirm OSM tile usage-policy compliance for downstream clients.

The server only computes addresses, but the returned source/tile_url point clients at tile.openstreetmap.org, whose usage policy requires a valid identifying User-Agent/Referer, mandatory attribution, and no bulk/heavy downloading. Since this endpoint effectively directs client traffic there, ensure the consuming client honors those terms (or route through a self-hosted/commercial tile provider) to avoid getting blocked.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cockpit-server/src/osm_tiles.rs` around lines 25 - 28, The OSM tile
URL constant in OSM_TILE_URL is fine as an address template, but the returned
source/tile_url must only be used by clients that comply with OpenStreetMap’s
usage policy. Update the code path around OSM_TILE_URL and any consumer of the
returned tile source so downstream clients send a valid identifying
User-Agent/Referer, include required attribution, and avoid bulk/heavy requests;
if that cannot be guaranteed, switch to a self-hosted or commercial tile
provider instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/cockpit-server/src/osm_tiles.rs`:
- Around line 163-176: The osm_tile_meta_handler currently accepts raw Path
coordinates and can emit misleading metadata for out-of-range x/y values. Add a
bounds check in osm_tile_meta_handler (before calling tile_to_hhtl,
resolved_tile, and tile_url) to reject x or y outside 0..2^z, or the equivalent
resolved 0..2^24 range, and return a 400 response instead of JSON for invalid
tile coordinates.
- Around line 121-128: `resolved_tile` can panic or compute the wrong parent
when `z - HHTL_DEPTH` is 32 or more, so update the shift logic to use a
checked/saturating right shift before returning the ancestor tile. Make the fix
inside `resolved_tile(z, x, y)` and ensure large zoom values safely clamp to the
HHTL_DEPTH ancestor with x/y resolving to 0 when the shift is too wide. Add a
regression test for the `resolved_tile` case with very large inputs, including
`resolved_tile(56, u32::MAX, u32::MAX)`, to verify it returns the expected
clamped tile.

---

Nitpick comments:
In `@crates/cockpit-server/src/osm_tiles.rs`:
- Around line 25-28: The OSM tile URL constant in OSM_TILE_URL is fine as an
address template, but the returned source/tile_url must only be used by clients
that comply with OpenStreetMap’s usage policy. Update the code path around
OSM_TILE_URL and any consumer of the returned tile source so downstream clients
send a valid identifying User-Agent/Referer, include required attribution, and
avoid bulk/heavy requests; if that cannot be guaranteed, switch to a self-hosted
or commercial tile provider instead.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b3dc5b7f-af88-45db-97c7-7d74bc697c2b

📥 Commits

Reviewing files that changed from the base of the PR and between 54dcce0 and b14f387.

📒 Files selected for processing (2)
  • crates/cockpit-server/src/main.rs
  • crates/cockpit-server/src/osm_tiles.rs

Comment on lines +121 to +128
pub fn resolved_tile(z: u32, x: u32, y: u32) -> (u32, u32, u32) {
if z > HHTL_DEPTH {
let excess = z - HHTL_DEPTH;
(HHTL_DEPTH, x >> excess, y >> excess)
} else {
(z, x, y)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate the relevant file and inspect the surrounding implementation.
git ls-files 'crates/cockpit-server/src/osm_tiles.rs'
echo '---'
wc -l crates/cockpit-server/src/osm_tiles.rs
echo '---'
sed -n '1,260p' crates/cockpit-server/src/osm_tiles.rs

Repository: AdaWorldAPI/q2

Length of output: 10125


🏁 Script executed:

#!/bin/bash
set -euo pipefail

cat > /tmp/shift_check.rs <<'RS'
fn main() {
    let x = 1u32;
    let s = 32u32;
    println!("{}", x >> s);
}
RS

echo "rustc version:"
rustc --version || true
echo '--- debug build ---'
rustc /tmp/shift_check.rs -o /tmp/shift_check_debug 2>/tmp/shift_check_debug.err && /tmp/shift_check_debug || { code=$?; cat /tmp/shift_check_debug.err; echo "exit=$code"; }
echo '--- release-like build ---'
rustc -O /tmp/shift_check.rs -o /tmp/shift_check_release 2>/tmp/shift_check_release.err && /tmp/shift_check_release || { code=$?; cat /tmp/shift_check_release.err; echo "exit=$code"; }

Repository: AdaWorldAPI/q2

Length of output: 352


Guard resolved_tile against over-wide shifts
z > 24 can make excess >= 32, so x >> excess / y >> excess may panic in debug builds and produce the wrong ancestor on GET /api/osm/tile/:z/:x/:y. Use a checked/saturating shift and add a regression test for resolved_tile(56, u32::MAX, u32::MAX) == (24, 0, 0).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cockpit-server/src/osm_tiles.rs` around lines 121 - 128,
`resolved_tile` can panic or compute the wrong parent when `z - HHTL_DEPTH` is
32 or more, so update the shift logic to use a checked/saturating right shift
before returning the ancestor tile. Make the fix inside `resolved_tile(z, x, y)`
and ensure large zoom values safely clamp to the HHTL_DEPTH ancestor with x/y
resolving to 0 when the shift is too wide. Add a regression test for the
`resolved_tile` case with very large inputs, including `resolved_tile(56,
u32::MAX, u32::MAX)`, to verify it returns the expected clamped tile.

Source: Coding guidelines

Comment on lines +163 to +176
pub async fn osm_tile_meta_handler(
Path((z, x, y)): Path<(u32, u32, u32)>,
) -> Json<serde_json::Value> {
let hhtl = tile_to_hhtl(z, x, y);
let (rz, rx, ry) = resolved_tile(z, x, y);
Json(serde_json::json!({
"z": z, "x": x, "y": y,
"tile_url": tile_url(z, x, y),
"source": OSM_TILE_URL,
"hhtl": hhtl,
"resolved": { "z": rz, "x": rx, "y": ry },
"geo_domain": "0x0F",
}))
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Validate x/y against 2^z in the tile-meta handler.

z, x, and y are taken raw from the path with no range check, so out-of-range coordinates (e.g. /api/osm/tile/2/999/1) flow into tile_to_hhtl, where x << shift truncates the high bits and yields a garbage HHTL key / tile_url that silently describes a different tile. Reject coordinates outside 0..2^z (or 0..2^24 after resolving) with a 400 instead of returning misleading metadata. (The z bound is covered by the resolved_tile fix above.)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/cockpit-server/src/osm_tiles.rs` around lines 163 - 176, The
osm_tile_meta_handler currently accepts raw Path coordinates and can emit
misleading metadata for out-of-range x/y values. Add a bounds check in
osm_tile_meta_handler (before calling tile_to_hhtl, resolved_tile, and tile_url)
to reject x or y outside 0..2^z, or the equivalent resolved 0..2^24 range, and
return a 400 response instead of JSON for invalid tile coordinates.

@AdaWorldAPI AdaWorldAPI merged commit 47f5c1d into main Jul 5, 2026
4 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