From ba58a5ec50286ce4b06ac483b2eff9ddcc12aa91 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Sun, 11 Oct 2026 09:40:05 -0400 Subject: [PATCH 1/4] fix(street-level): deduplicate overlapping dateline tile halves --- CHANGELOG.md | 2 ++ docs/CURRENT-STATE.md | 5 +++++ .../street-level-unique-tiles.test.mjs | 15 +++++++++++++++ src/layers/streetLevel/tileMath.js | 7 ++++++- 4 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/layers/streetLevel/street-level-unique-tiles.test.mjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7acdedf6b..0814f7e7c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is unchanged; extra cameras share it round-robin with the other packs. +- Street Level requests count each tile once when dateline bounds overlap at coarse zoom levels. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index c77d1d8e3..d0014227f 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -5310,3 +5310,8 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. + + +### fix(street-level): deduplicate overlapping dateline tile halves + +Deduplicate tile addresses before ranking, truncating and reporting total coverage. Dateline ordering and the existing focus policy remain in control. diff --git a/src/layers/streetLevel/street-level-unique-tiles.test.mjs b/src/layers/streetLevel/street-level-unique-tiles.test.mjs new file mode 100644 index 000000000..f96f0b892 --- /dev/null +++ b/src/layers/streetLevel/street-level-unique-tiles.test.mjs @@ -0,0 +1,15 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { tilesForBbox } from './tileMath.js'; +test('coarse dateline coverage counts each tile exactly once', () => { + for (const z of [0, 1, 2]) { + const result = tilesForBbox([170, -1, -170, 1], z); + const keys = result.tiles.map((t) => `${t.x}/${t.y}/${t.z}`); + assert.equal(new Set(keys).size, keys.length); + assert.equal(result.total, keys.length); + const limited = tilesForBbox([170, -1, -170, 1], z, { limit: 1 }); + assert.equal(limited.total, keys.length); + assert.equal(limited.truncated, keys.length > 1); + assert.equal(limited.tiles.length, 1); + } +}); diff --git a/src/layers/streetLevel/tileMath.js b/src/layers/streetLevel/tileMath.js index fe3c3ec18..ca7b09cd5 100644 --- a/src/layers/streetLevel/tileMath.js +++ b/src/layers/streetLevel/tileMath.js @@ -147,7 +147,12 @@ export function tilesForBbox(bbox, z, { limit = Infinity, focus = null } = {}) { return Math.min(dx, n - dx) ** 2 + (tile.y - centre.y) ** 2; }; // Each tile's distance is computed once; ties keep their row order. - const ranked = tiles + const unique = [ + ...new Map( + tiles.map((tile) => [`${tile.x}/${tile.y}/${tile.z}`, tile]), + ).values(), + ]; + const ranked = unique .map((tile) => ({ tile, d: distance2(tile) })) .sort((a, b) => a.d - b.d) .map(({ tile }) => tile); From f67888be00327c45bb2f0a651979ea9eb77489f4 Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Mon, 12 Oct 2026 11:12:01 -0400 Subject: [PATCH 2/4] docs: address review scope and feature placement for #1032 --- CHANGELOG.md | 3 ++ docs/CURRENT-STATE.md | 9 +++--- .../street-level-unique-tiles.test.mjs | 29 ++++++++++++------- 3 files changed, 25 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0814f7e7c..1d76a1963 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,9 @@ - Street Level requests count each tile once when dateline bounds overlap at coarse zoom levels. +- Street Level counts unique tiles before ranking so overlapping dateline halves + in polar views do not waste slots in the 16-tile overview budget. + - Treat explicitly current, complete empty vessel coverage as a successful update, clearing obsolete contacts while retaining stale or incomplete snapshots. diff --git a/docs/CURRENT-STATE.md b/docs/CURRENT-STATE.md index d0014227f..c2291829e 100644 --- a/docs/CURRENT-STATE.md +++ b/docs/CURRENT-STATE.md @@ -3122,6 +3122,10 @@ sequence lines below 60 km; the proxy strips the unused `image` layer from z14 tiles (12 MB → ~80 KB) and shares one upstream fetch between concurrent requests for a tile. +Mapillary coverage ranks unique tile addresses before applying the 16-tile +overview budget. Overlapping dateline halves in a polar view therefore do not +waste overview slots on the same tile; loading already skips duplicate keys. + On Google 3D (`photoreal`) at street zoom (in below 1,400 m above ground, out above 1,800 m) the surface mode switches from `draped` to `terrain`: draped lines land on roofs and tree tops, so `groundCast.js` instead places lines, @@ -5310,8 +5314,3 @@ datacenters, dams and the Nepal locator retain that shared credit while displaye Military fragments stitch by source identity. Different mapped parcels merge only when their footprints touch or contain one another, never across a seam tolerance gap. - - -### fix(street-level): deduplicate overlapping dateline tile halves - -Deduplicate tile addresses before ranking, truncating and reporting total coverage. Dateline ordering and the existing focus policy remain in control. diff --git a/src/layers/streetLevel/street-level-unique-tiles.test.mjs b/src/layers/streetLevel/street-level-unique-tiles.test.mjs index f96f0b892..224de523b 100644 --- a/src/layers/streetLevel/street-level-unique-tiles.test.mjs +++ b/src/layers/streetLevel/street-level-unique-tiles.test.mjs @@ -1,15 +1,22 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { tilesForBbox } from './tileMath.js'; -test('coarse dateline coverage counts each tile exactly once', () => { - for (const z of [0, 1, 2]) { - const result = tilesForBbox([170, -1, -170, 1], z); - const keys = result.tiles.map((t) => `${t.x}/${t.y}/${t.z}`); - assert.equal(new Set(keys).size, keys.length); - assert.equal(result.total, keys.length); - const limited = tilesForBbox([170, -1, -170, 1], z, { limit: 1 }); - assert.equal(limited.total, keys.length); - assert.equal(limited.truncated, keys.length > 1); - assert.equal(limited.tiles.length, 1); - } + +test('reachable polar dateline coverage ranks 24 distinct tiles before overview truncation', () => { + // coverage uses the actual view bbox above zoom 1; polar views can wrap. + const bbox = [-10, 60, -20, 85]; + const result = tilesForBbox(bbox, 3); + const keys = result.tiles.map((tile) => `${tile.x}/${tile.y}/${tile.z}`); + assert.equal(keys.length, 24); + assert.equal(new Set(keys).size, 24); + assert.equal(result.total, 24); + assert.equal(result.truncated, false); + const overview = tilesForBbox(bbox, 3, { limit: 16 }); + assert.equal(overview.total, 24); + assert.equal(overview.tiles.length, 16); + assert.equal( + new Set(overview.tiles.map((tile) => `${tile.x}/${tile.y}/${tile.z}`)).size, + 16, + ); + assert.equal(overview.truncated, true); }); From 4adcb3f7274951bd5c763cc59e190ef559c169de Mon Sep 17 00:00:00 2001 From: Rudy Celekli Date: Wed, 14 Oct 2026 15:18:17 -0400 Subject: [PATCH 3/4] docs: drop superseded CHANGELOG bullet The review-driven rewrite added a narrower entry but left the original bullet in place, so the PR carried two entries for one change. Co-Authored-By: Claude Sonnet 5.5 --- CHANGELOG.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d76a1963..69748384f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,8 +15,6 @@ the five borough cores load and `CCTV_NYC_MAX_SOURCES` changes the cap. The catalog-wide ceiling is unchanged; extra cameras share it round-robin with the other packs. -- Street Level requests count each tile once when dateline bounds overlap at coarse zoom levels. - - Street Level counts unique tiles before ranking so overlapping dateline halves in polar views do not waste slots in the 16-tile overview budget. From 84714030c86ca42a770aa04e19ae52fabf457dc8 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Wed, 14 Oct 2026 16:35:47 -0700 Subject: [PATCH 4/4] test: show a capped overview no longer holds repeat tiles Adds a case that fails before de-duplication (a 24-slot overview held repeats and reported itself truncated) and says why the date-line halves overlap. --- .../streetLevel/street-level-unique-tiles.test.mjs | 9 +++++++++ src/layers/streetLevel/tileMath.js | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/layers/streetLevel/street-level-unique-tiles.test.mjs b/src/layers/streetLevel/street-level-unique-tiles.test.mjs index 224de523b..52e7b6aea 100644 --- a/src/layers/streetLevel/street-level-unique-tiles.test.mjs +++ b/src/layers/streetLevel/street-level-unique-tiles.test.mjs @@ -20,3 +20,12 @@ test('reachable polar dateline coverage ranks 24 distinct tiles before overview ); assert.equal(overview.truncated, true); }); + +test('a limit that fits every distinct tile is not spent on repeats', () => { + // Before de-duplication this box yielded 27 entries for 24 tiles, so a + // 24-slot overview held repeats and still reported itself truncated. + const result = tilesForBbox([-10, 60, -20, 85], 3, { limit: 24 }); + const keys = result.tiles.map((tile) => `${tile.x}/${tile.y}/${tile.z}`); + assert.equal(new Set(keys).size, 24); + assert.equal(result.truncated, false); +}); diff --git a/src/layers/streetLevel/tileMath.js b/src/layers/streetLevel/tileMath.js index ca7b09cd5..350188200 100644 --- a/src/layers/streetLevel/tileMath.js +++ b/src/layers/streetLevel/tileMath.js @@ -146,7 +146,10 @@ export function tilesForBbox(bbox, z, { limit = Infinity, focus = null } = {}) { const dx = Math.abs(tile.x - centre.x); return Math.min(dx, n - dx) ** 2 + (tile.y - centre.y) ** 2; }; - // Each tile's distance is computed once; ties keep their row order. + // The date-line halves share a column when the gap between east and west is + // narrower than a tile; count that column once so a capped overview does not + // spend its slots on repeats. Each tile's distance is computed once; ties keep + // their row order. const unique = [ ...new Map( tiles.map((tile) => [`${tile.x}/${tile.y}/${tile.z}`, tile]),