From 27076bea2f7f9efeb82be2fb92be72a567b8d47f Mon Sep 17 00:00:00 2001 From: razbroc Date: Sat, 16 May 2026 23:39:32 +0300 Subject: [PATCH 1/7] fix: adjust bboxToTileRange to use exclusive upper bounds and update related tests Co-authored-by: Copilot --- src/geo/bboxUtils.ts | 4 ++-- src/geo/tileRanger.ts | 4 ++-- tests/unit/geo/bboxUtils.spec.ts | 16 ++++++++-------- tests/unit/geo/tileRanger.spec.ts | 4 ++-- 4 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/geo/bboxUtils.ts b/src/geo/bboxUtils.ts index febf4df..5fd2fec 100644 --- a/src/geo/bboxUtils.ts +++ b/src/geo/bboxUtils.ts @@ -90,8 +90,8 @@ export const bboxToTileRange = (bbox: BBox2d, zoom: number): ITileRange => { return { minX: minTile.x, minY: minTile.y, - maxX: maxTile.x, - maxY: maxTile.y, + maxX: maxTile.x - 1, // exclusive upper bound: maxTile is the first tile outside the bbox (right/above) + maxY: maxTile.y - 1, // exclusive upper bound: maxTile is the first tile outside the bbox (right/above) zoom, }; }; diff --git a/src/geo/tileRanger.ts b/src/geo/tileRanger.ts index 1fa2274..709609d 100644 --- a/src/geo/tileRanger.ts +++ b/src/geo/tileRanger.ts @@ -163,8 +163,8 @@ export class TileRanger { ///////////////////////////////////////////////////////////////////////////////////////////////// //find base hashes const minimalRange = bboxToTileRange(bbox, minZoom); - for (let x = minimalRange.minX; x < minimalRange.maxX; x++) { - for (let y = minimalRange.minY; y < minimalRange.maxY; y++) { + for (let x = minimalRange.minX; x <= minimalRange.maxX; x++) { + for (let y = minimalRange.minY; y <= minimalRange.maxY; y++) { ///////////////////////////////////////////////////////////////////////////////////////////////// /// Step 6: for every tile in the current range: /// Step 7: check the tile intersection with the footprint diff --git a/tests/unit/geo/bboxUtils.spec.ts b/tests/unit/geo/bboxUtils.spec.ts index 39addca..0221c3c 100644 --- a/tests/unit/geo/bboxUtils.spec.ts +++ b/tests/unit/geo/bboxUtils.spec.ts @@ -41,8 +41,8 @@ describe('bboxUtils', () => { const expectedRange = { minX: 4, minY: 2, - maxX: 5, - maxY: 3, + maxX: 4, + maxY: 2, zoom: 2, }; expect(range).toEqual(expectedRange); @@ -56,8 +56,8 @@ describe('bboxUtils', () => { const expectedRange = { minX: 4, minY: 2, - maxX: 6, - maxY: 3, + maxX: 5, + maxY: 2, zoom: 2, }; expect(range).toEqual(expectedRange); @@ -71,8 +71,8 @@ describe('bboxUtils', () => { const expectedRange = { minX: 2, minY: 1, - maxX: 3, - maxY: 2, + maxX: 2, + maxY: 1, zoom: 1, }; expect(range).toEqual(expectedRange); @@ -86,8 +86,8 @@ describe('bboxUtils', () => { const expectedRange = { minX: 8, minY: 4, - maxX: 10, - maxY: 7, + maxX: 9, + maxY: 6, zoom: 3, }; expect(range).toEqual(expectedRange); diff --git a/tests/unit/geo/tileRanger.spec.ts b/tests/unit/geo/tileRanger.spec.ts index 5b5d1af..738cac5 100644 --- a/tests/unit/geo/tileRanger.spec.ts +++ b/tests/unit/geo/tileRanger.spec.ts @@ -82,9 +82,9 @@ describe('TileRanger', () => { const expectedRanges = [ { minX: 3, - maxX: 5, + maxX: 4, minY: 2, - maxY: 3, + maxY: 2, zoom: 2, }, ]; From 5835cf9ce64c59554123baad2491e6490b196b87 Mon Sep 17 00:00:00 2001 From: razbroc Date: Sun, 17 May 2026 11:53:24 +0300 Subject: [PATCH 2/7] fix: correct tile range calculations and update related tests Co-authored-by: Copilot --- src/geo/tileRanger.ts | 12 +++--- src/geo/tiles.ts | 2 +- src/geo/tilesGenerator.ts | 4 +- tests/unit/geo/tileRanger.spec.ts | 72 +++++++++++++++---------------- tests/unit/geo/tiles.spec.ts | 2 +- 5 files changed, 46 insertions(+), 46 deletions(-) diff --git a/src/geo/tileRanger.ts b/src/geo/tileRanger.ts index 709609d..dcb64e4 100644 --- a/src/geo/tileRanger.ts +++ b/src/geo/tileRanger.ts @@ -32,21 +32,21 @@ export class TileRanger { public tileToRange(tile: ITile, zoom: number): ITileRange { let minX: number, minY: number, maxX: number, maxY: number; minX = tile.x; - maxX = tile.x + 1; + maxX = tile.x; minY = tile.y; - maxY = tile.y + 1; + maxY = tile.y; if (tile.zoom < zoom) { const dz = zoom - tile.zoom; minX = minX << dz; - maxX = maxX << dz; + maxX = ((tile.x + 1) << dz) - 1; minY = minY << dz; - maxY = maxY << dz; + maxY = ((tile.y + 1) << dz) - 1; } else if (tile.zoom > zoom) { const dz = tile.zoom - zoom; minX = minX >> dz; minY = minY >> dz; - maxX = minX + 1; - maxY = minY + 1; + maxX = minX; + maxY = minY; } return { minX, diff --git a/src/geo/tiles.ts b/src/geo/tiles.ts index ac38933..6d5e860 100644 --- a/src/geo/tiles.ts +++ b/src/geo/tiles.ts @@ -143,7 +143,7 @@ export function tileToBbox(tile: ITile): BBox2d { * @returns */ export function tileRangeToTilesCount(batch: ITileRange): number { - return (batch.maxX - batch.minX) * (batch.maxY - batch.minY); + return (batch.maxX - batch.minX + 1) * (batch.maxY - batch.minY + 1); } /** diff --git a/src/geo/tilesGenerator.ts b/src/geo/tilesGenerator.ts index 97d912f..79da9f1 100644 --- a/src/geo/tilesGenerator.ts +++ b/src/geo/tilesGenerator.ts @@ -2,8 +2,8 @@ import { ITile, ITileRange } from '../models/interfaces/geo/iTile'; export async function* tilesGenerator(rangeGen: AsyncIterable): AsyncGenerator { for await (const range of rangeGen) { - for (let x = range.minX; x < range.maxX; x++) { - for (let y = range.minY; y < range.maxY; y++) { + for (let x = range.minX; x <= range.maxX; x++) { + for (let y = range.minY; y <= range.maxY; y++) { yield await Promise.resolve({ x, y, diff --git a/tests/unit/geo/tileRanger.spec.ts b/tests/unit/geo/tileRanger.spec.ts index 738cac5..cc4058e 100644 --- a/tests/unit/geo/tileRanger.spec.ts +++ b/tests/unit/geo/tileRanger.spec.ts @@ -22,8 +22,8 @@ describe('TileRanger', () => { const expectedRange = { minX: 1, minY: 1, - maxX: 2, - maxY: 2, + maxX: 1, + maxY: 1, zoom: 2, }; expect(range).toEqual(expectedRange); @@ -41,8 +41,8 @@ describe('TileRanger', () => { const expectedRange = { minX: 2, minY: 2, - maxX: 4, - maxY: 4, + maxX: 3, + maxY: 3, zoom: 3, }; expect(range).toEqual(expectedRange); @@ -60,8 +60,8 @@ describe('TileRanger', () => { const expectedRange = { minX: 0, minY: 0, - maxX: 1, - maxY: 1, + maxX: 0, + maxY: 0, zoom: 1, }; expect(range).toEqual(expectedRange); @@ -108,36 +108,36 @@ describe('TileRanger', () => { } const expectedRanges = [ - { minX: 24, minY: 16, maxX: 25, maxY: 17, zoom: 5 }, - { minX: 25, minY: 16, maxX: 26, maxY: 17, zoom: 5 }, - { minX: 25, minY: 17, maxX: 26, maxY: 18, zoom: 5 }, - { minX: 26, minY: 16, maxX: 28, maxY: 18, zoom: 5 }, - { minX: 26, minY: 18, maxX: 27, maxY: 19, zoom: 5 }, - { minX: 27, minY: 18, maxX: 28, maxY: 19, zoom: 5 }, - { minX: 27, minY: 19, maxX: 28, maxY: 20, zoom: 5 }, - { minX: 28, minY: 16, maxX: 32, maxY: 20, zoom: 5 }, - { minX: 28, minY: 20, maxX: 29, maxY: 21, zoom: 5 }, - { minX: 29, minY: 20, maxX: 30, maxY: 21, zoom: 5 }, - { minX: 29, minY: 21, maxX: 30, maxY: 22, zoom: 5 }, - { minX: 30, minY: 20, maxX: 32, maxY: 22, zoom: 5 }, - { minX: 30, minY: 22, maxX: 31, maxY: 23, zoom: 5 }, - { minX: 31, minY: 22, maxX: 32, maxY: 23, zoom: 5 }, - { minX: 31, minY: 23, maxX: 32, maxY: 24, zoom: 5 }, - { minX: 32, minY: 16, maxX: 36, maxY: 20, zoom: 5 }, - { minX: 36, minY: 16, maxX: 38, maxY: 18, zoom: 5 }, - { minX: 38, minY: 16, maxX: 39, maxY: 17, zoom: 5 }, - { minX: 39, minY: 16, maxX: 40, maxY: 17, zoom: 5 }, - { minX: 38, minY: 17, maxX: 39, maxY: 18, zoom: 5 }, - { minX: 36, minY: 18, maxX: 37, maxY: 19, zoom: 5 }, - { minX: 37, minY: 18, maxX: 38, maxY: 19, zoom: 5 }, - { minX: 36, minY: 19, maxX: 37, maxY: 20, zoom: 5 }, - { minX: 32, minY: 20, maxX: 34, maxY: 22, zoom: 5 }, - { minX: 34, minY: 20, maxX: 35, maxY: 21, zoom: 5 }, - { minX: 35, minY: 20, maxX: 36, maxY: 21, zoom: 5 }, - { minX: 34, minY: 21, maxX: 35, maxY: 22, zoom: 5 }, - { minX: 32, minY: 22, maxX: 33, maxY: 23, zoom: 5 }, - { minX: 33, minY: 22, maxX: 34, maxY: 23, zoom: 5 }, - { minX: 32, minY: 23, maxX: 33, maxY: 24, zoom: 5 }, + { minX: 24, minY: 16, maxX: 24, maxY: 16, zoom: 5 }, + { minX: 25, minY: 16, maxX: 25, maxY: 16, zoom: 5 }, + { minX: 25, minY: 17, maxX: 25, maxY: 17, zoom: 5 }, + { minX: 26, minY: 16, maxX: 27, maxY: 17, zoom: 5 }, + { minX: 26, minY: 18, maxX: 26, maxY: 18, zoom: 5 }, + { minX: 27, minY: 18, maxX: 27, maxY: 18, zoom: 5 }, + { minX: 27, minY: 19, maxX: 27, maxY: 19, zoom: 5 }, + { minX: 28, minY: 16, maxX: 31, maxY: 19, zoom: 5 }, + { minX: 28, minY: 20, maxX: 28, maxY: 20, zoom: 5 }, + { minX: 29, minY: 20, maxX: 29, maxY: 20, zoom: 5 }, + { minX: 29, minY: 21, maxX: 29, maxY: 21, zoom: 5 }, + { minX: 30, minY: 20, maxX: 31, maxY: 21, zoom: 5 }, + { minX: 30, minY: 22, maxX: 30, maxY: 22, zoom: 5 }, + { minX: 31, minY: 22, maxX: 31, maxY: 22, zoom: 5 }, + { minX: 31, minY: 23, maxX: 31, maxY: 23, zoom: 5 }, + { minX: 32, minY: 16, maxX: 35, maxY: 19, zoom: 5 }, + { minX: 36, minY: 16, maxX: 37, maxY: 17, zoom: 5 }, + { minX: 38, minY: 16, maxX: 38, maxY: 16, zoom: 5 }, + { minX: 39, minY: 16, maxX: 39, maxY: 16, zoom: 5 }, + { minX: 38, minY: 17, maxX: 38, maxY: 17, zoom: 5 }, + { minX: 36, minY: 18, maxX: 36, maxY: 18, zoom: 5 }, + { minX: 37, minY: 18, maxX: 37, maxY: 18, zoom: 5 }, + { minX: 36, minY: 19, maxX: 36, maxY: 19, zoom: 5 }, + { minX: 32, minY: 20, maxX: 33, maxY: 21, zoom: 5 }, + { minX: 34, minY: 20, maxX: 34, maxY: 20, zoom: 5 }, + { minX: 35, minY: 20, maxX: 35, maxY: 20, zoom: 5 }, + { minX: 34, minY: 21, maxX: 34, maxY: 21, zoom: 5 }, + { minX: 32, minY: 22, maxX: 32, maxY: 22, zoom: 5 }, + { minX: 33, minY: 22, maxX: 33, maxY: 22, zoom: 5 }, + { minX: 32, minY: 23, maxX: 32, maxY: 23, zoom: 5 }, ]; expect(tileRanges).toEqual(expectedRanges); }); diff --git a/tests/unit/geo/tiles.spec.ts b/tests/unit/geo/tiles.spec.ts index ad4e8e2..4761095 100644 --- a/tests/unit/geo/tiles.spec.ts +++ b/tests/unit/geo/tiles.spec.ts @@ -121,7 +121,7 @@ describe('tiles', () => { zoom: 0, }; const areaResult = tileRangeToTilesCount(batch); - const expectedResult = 64800; + const expectedResult = 65341; expect(areaResult).toEqual(expectedResult); }); From af2bf5c8e0c1c5f0cc8040f01551ceddafa88d20 Mon Sep 17 00:00:00 2001 From: razbroc Date: Sun, 17 May 2026 16:16:11 +0300 Subject: [PATCH 3/7] fix: update tileBatchGenerator logic to correctly handle tile ranges and adjust related tests Co-authored-by: Copilot --- src/geo/tileBatcher.ts | 43 +++++++++++++++--------------- tests/unit/geo/tileBatcher.spec.ts | 40 +++++++++++++-------------- 2 files changed, 41 insertions(+), 42 deletions(-) diff --git a/src/geo/tileBatcher.ts b/src/geo/tileBatcher.ts index dcf12fa..15f2d25 100644 --- a/src/geo/tileBatcher.ts +++ b/src/geo/tileBatcher.ts @@ -10,22 +10,22 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator requiredForFullBatch) { targetRanges.push({ minX: reminderX, - maxX: reminderX + requiredForFullBatch, + maxX: reminderX + requiredForFullBatch - 1, minY: range.minY, - maxY: range.minY + 1, + maxY: range.minY, zoom: range.zoom, }); yield await Promise.resolve(targetRanges); @@ -36,13 +36,13 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator 0 && range.minY < range.maxY) { - const endX = Math.min(range.minX + requiredForFullBatch, range.maxX); + //add partial line at row beginning + if (requiredForFullBatch > 0 && range.minY <= range.maxY) { + const endX = Math.min(range.minX + requiredForFullBatch - 1, range.maxX); targetRanges.push({ minX: range.minX, maxX: endX, minY: range.minY, - maxY: range.minY + 1, + maxY: range.minY, zoom: range.zoom, }); - requiredForFullBatch -= endX - range.minX; - if (endX < range.maxX) { - reminderX = endX; - } else { + requiredForFullBatch -= endX - range.minX + 1; + reminderX = endX + 1; // next partial start, or sentinel if endX === maxX + if (endX >= range.maxX) { range.minY++; } } diff --git a/tests/unit/geo/tileBatcher.spec.ts b/tests/unit/geo/tileBatcher.spec.ts index 2f14600..e8f9d5e 100644 --- a/tests/unit/geo/tileBatcher.spec.ts +++ b/tests/unit/geo/tileBatcher.spec.ts @@ -5,9 +5,9 @@ describe('GeoHashBatcher', () => { describe('#getResource', () => { it('return expected batches for complex data', async function () { const ranges = [ - { minX: 0, minY: 2, maxX: 5, maxY: 4, zoom: 8 }, - { minX: 0, minY: 6, maxX: 2, maxY: 8, zoom: 8 }, - { minX: 0, minY: 8, maxX: 1, maxY: 11, zoom: 8 }, + { minX: 0, minY: 2, maxX: 4, maxY: 3, zoom: 8 }, + { minX: 0, minY: 6, maxX: 1, maxY: 7, zoom: 8 }, + { minX: 0, minY: 8, maxX: 0, maxY: 10, zoom: 8 }, ]; const rangeAsyncGen = (async function* () { @@ -25,27 +25,27 @@ describe('GeoHashBatcher', () => { // expectation const expectedBatches = [ - [{ minX: 0, maxX: 3, minY: 2, maxY: 3, zoom: 8 }], + [{ minX: 0, maxX: 2, minY: 2, maxY: 2, zoom: 8 }], [ - { minX: 3, maxX: 5, minY: 2, maxY: 3, zoom: 8 }, - { minX: 0, maxX: 1, minY: 3, maxY: 4, zoom: 8 }, + { minX: 3, maxX: 4, minY: 2, maxY: 2, zoom: 8 }, + { minX: 0, maxX: 0, minY: 3, maxY: 3, zoom: 8 }, ], - [{ minX: 1, maxX: 4, minY: 3, maxY: 4, zoom: 8 }], + [{ minX: 1, maxX: 3, minY: 3, maxY: 3, zoom: 8 }], [ - { minX: 4, maxX: 5, minY: 3, maxY: 4, zoom: 8 }, - { minX: 0, maxX: 2, minY: 6, maxY: 7, zoom: 8 }, + { minX: 4, maxX: 4, minY: 3, maxY: 3, zoom: 8 }, + { minX: 0, maxX: 1, minY: 6, maxY: 6, zoom: 8 }, ], [ - { minX: 0, maxX: 2, minY: 7, maxY: 8, zoom: 8 }, - { minX: 0, maxX: 1, minY: 8, maxY: 9, zoom: 8 }, + { minX: 0, maxX: 1, minY: 7, maxY: 7, zoom: 8 }, + { minX: 0, maxX: 0, minY: 8, maxY: 8, zoom: 8 }, ], - [{ minX: 0, maxX: 1, minY: 9, maxY: 11, zoom: 8 }], + [{ minX: 0, maxX: 0, minY: 9, maxY: 10, zoom: 8 }], ]; expect(batches).toEqual(expectedBatches); }); it('return expected batch for single tile', async function () { - const ranges = [{ minX: 0, minY: 2, maxX: 1, maxY: 3, zoom: 8 }]; + const ranges = [{ minX: 0, minY: 2, maxX: 0, maxY: 2, zoom: 8 }]; const rangeAsyncGen = (async function* () { yield await Promise.resolve(ranges[0]); })(); @@ -58,12 +58,12 @@ describe('GeoHashBatcher', () => { } // expectation - const expectedBatches = [[{ minX: 0, maxX: 1, minY: 2, maxY: 3, zoom: 8 }]]; + const expectedBatches = [[{ minX: 0, maxX: 0, minY: 2, maxY: 2, zoom: 8 }]]; expect(batches).toEqual(expectedBatches); }); it('return empty batch on invalid empty x', async function () { - const ranges = [{ minX: 0, minY: 2, maxX: 0, maxY: 3, zoom: 8 }]; + const ranges = [{ minX: 1, minY: 2, maxX: 0, maxY: 3, zoom: 8 }]; const rangeAsyncGen = (async function* () { yield await Promise.resolve(ranges[0]); })(); @@ -81,7 +81,7 @@ describe('GeoHashBatcher', () => { }); it('return empty batch on invalid empty y', async function () { - const ranges = [{ minX: 0, minY: 2, maxX: 4, maxY: 2, zoom: 8 }]; + const ranges = [{ minX: 0, minY: 3, maxX: 4, maxY: 2, zoom: 8 }]; const rangeAsyncGen = (async function* () { yield await Promise.resolve(ranges[0]); })(); @@ -99,7 +99,7 @@ describe('GeoHashBatcher', () => { }); it('return proper tiles for batch size that is power of 2', async function () { - const ranges = [{ minX: 0, minY: 16, maxX: 16, maxY: 32, zoom: 5 }]; + const ranges = [{ minX: 0, minY: 16, maxX: 15, maxY: 31, zoom: 5 }]; const rangeAsyncGen = (async function* () { yield await Promise.resolve(ranges[0]); })(); @@ -113,9 +113,9 @@ describe('GeoHashBatcher', () => { // expectation const expectedBatches: ITileRange[][] = []; - for (let y = 16; y < 32; y++) { - for (let x = 0; x < 16; x++) { - expectedBatches.push([{ minX: x, maxX: x + 1, minY: y, maxY: y + 1, zoom: 5 }]); + for (let y = 16; y <= 31; y++) { + for (let x = 0; x <= 15; x++) { + expectedBatches.push([{ minX: x, maxX: x, minY: y, maxY: y, zoom: 5 }]); } } expect(batches).toEqual(expectedBatches); From 9cb15515304d2f7418a1a090c532af596c4b65eb Mon Sep 17 00:00:00 2001 From: razbroc Date: Sun, 17 May 2026 16:23:24 +0300 Subject: [PATCH 4/7] fix: enhance tileBatchGenerator and tilesGenerator logic for inclusive tile range handling Co-authored-by: Copilot --- src/geo/tileBatcher.ts | 19 +++++++++++-------- src/geo/tileRanger.ts | 6 +++--- src/geo/tiles.ts | 2 +- src/geo/tilesGenerator.ts | 2 ++ 4 files changed, 17 insertions(+), 12 deletions(-) diff --git a/src/geo/tileBatcher.ts b/src/geo/tileBatcher.ts index 15f2d25..77472b2 100644 --- a/src/geo/tileBatcher.ts +++ b/src/geo/tileBatcher.ts @@ -10,16 +10,19 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator maxX means sentinel (no partial row) + const remaining = range.maxX - reminderX + 1; // +1: maxX is inclusive if (remaining > requiredForFullBatch) { targetRanges.push({ minX: reminderX, @@ -42,7 +45,7 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator= range.maxX) { range.minY++; } diff --git a/src/geo/tileRanger.ts b/src/geo/tileRanger.ts index dcb64e4..60ac31c 100644 --- a/src/geo/tileRanger.ts +++ b/src/geo/tileRanger.ts @@ -38,14 +38,14 @@ export class TileRanger { if (tile.zoom < zoom) { const dz = zoom - tile.zoom; minX = minX << dz; - maxX = ((tile.x + 1) << dz) - 1; + maxX = ((tile.x + 1) << dz) - 1; // (tile.x+1) is the exclusive next tile; shift then -1 gives the inclusive last sub-tile minY = minY << dz; - maxY = ((tile.y + 1) << dz) - 1; + maxY = ((tile.y + 1) << dz) - 1; // same logic for Y axis } else if (tile.zoom > zoom) { const dz = tile.zoom - zoom; minX = minX >> dz; minY = minY >> dz; - maxX = minX; + maxX = minX; // tile maps to a single parent tile; inclusive range collapses to one tile maxY = minY; } return { diff --git a/src/geo/tiles.ts b/src/geo/tiles.ts index 6d5e860..2f8b432 100644 --- a/src/geo/tiles.ts +++ b/src/geo/tiles.ts @@ -143,7 +143,7 @@ export function tileToBbox(tile: ITile): BBox2d { * @returns */ export function tileRangeToTilesCount(batch: ITileRange): number { - return (batch.maxX - batch.minX + 1) * (batch.maxY - batch.minY + 1); + return (batch.maxX - batch.minX + 1) * (batch.maxY - batch.minY + 1); // +1 on each axis because maxX/maxY are inclusive } /** diff --git a/src/geo/tilesGenerator.ts b/src/geo/tilesGenerator.ts index 79da9f1..d7b77f9 100644 --- a/src/geo/tilesGenerator.ts +++ b/src/geo/tilesGenerator.ts @@ -3,7 +3,9 @@ import { ITile, ITileRange } from '../models/interfaces/geo/iTile'; export async function* tilesGenerator(rangeGen: AsyncIterable): AsyncGenerator { for await (const range of rangeGen) { for (let x = range.minX; x <= range.maxX; x++) { + // <= because maxX is inclusive for (let y = range.minY; y <= range.maxY; y++) { + // <= because maxY is inclusive yield await Promise.resolve({ x, y, From c34d30632e62310c8af94800b24a037fbd28ea01 Mon Sep 17 00:00:00 2001 From: razbroc Date: Mon, 18 May 2026 11:34:50 +0300 Subject: [PATCH 5/7] fix: rename tileRangeToTilesCount to tileRangeSize for clarity and update related tests --- src/geo/tiles.ts | 4 ++-- src/geo/tilesGenerator.ts | 2 -- tests/unit/geo/tiles.spec.ts | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/geo/tiles.ts b/src/geo/tiles.ts index 2f8b432..6f64de0 100644 --- a/src/geo/tiles.ts +++ b/src/geo/tiles.ts @@ -142,7 +142,7 @@ export function tileToBbox(tile: ITile): BBox2d { * @param ITileRange * @returns */ -export function tileRangeToTilesCount(batch: ITileRange): number { +export function tileRangeSize(batch: ITileRange): number { return (batch.maxX - batch.minX + 1) * (batch.maxY - batch.minY + 1); // +1 on each axis because maxX/maxY are inclusive } @@ -182,7 +182,7 @@ export function featureToTilesCount(feature: Feature, de for (let i = targetMinZoom; i <= targetMaxZoom; i++) { const zoomTilesBatch = bboxToTileRange(sanitized, i); - tilesTotalAmount += tileRangeToTilesCount(zoomTilesBatch); + tilesTotalAmount += tileRangeSize(zoomTilesBatch); } return tilesTotalAmount; diff --git a/src/geo/tilesGenerator.ts b/src/geo/tilesGenerator.ts index d7b77f9..79da9f1 100644 --- a/src/geo/tilesGenerator.ts +++ b/src/geo/tilesGenerator.ts @@ -3,9 +3,7 @@ import { ITile, ITileRange } from '../models/interfaces/geo/iTile'; export async function* tilesGenerator(rangeGen: AsyncIterable): AsyncGenerator { for await (const range of rangeGen) { for (let x = range.minX; x <= range.maxX; x++) { - // <= because maxX is inclusive for (let y = range.minY; y <= range.maxY; y++) { - // <= because maxY is inclusive yield await Promise.resolve({ x, y, diff --git a/tests/unit/geo/tiles.spec.ts b/tests/unit/geo/tiles.spec.ts index 4761095..fe0454a 100644 --- a/tests/unit/geo/tiles.spec.ts +++ b/tests/unit/geo/tiles.spec.ts @@ -1,7 +1,7 @@ import type { Feature, MultiPolygon, Polygon } from 'geojson'; import { ITileRange } from '../../../src'; import { - tileRangeToTilesCount, + tileRangeSize, degreesPerPixel, degreesPerPixelToZoomLevel, degreesPerTile, @@ -120,7 +120,7 @@ describe('tiles', () => { minY: -90, zoom: 0, }; - const areaResult = tileRangeToTilesCount(batch); + const areaResult = tileRangeSize(batch); const expectedResult = 65341; expect(areaResult).toEqual(expectedResult); }); From d1fc653aa1ed006027d80838ef020a2f672c500e Mon Sep 17 00:00:00 2001 From: razbroc Date: Mon, 18 May 2026 14:07:13 +0300 Subject: [PATCH 6/7] fix: pr comments Co-authored-by: Copilot --- docs/modules.html | 4 ++-- src/geo/bboxUtils.ts | 2 +- src/geo/tileBatcher.ts | 15 +++++++-------- src/geo/tiles.ts | 2 +- tests/unit/geo/bboxUtils.spec.ts | 8 ++++---- tests/unit/geo/tileBatcher.spec.ts | 2 +- tests/unit/geo/tiles.spec.ts | 13 +++++++------ 7 files changed, 23 insertions(+), 23 deletions(-) diff --git a/docs/modules.html b/docs/modules.html index f4ed934..108d9ff 100644 --- a/docs/modules.html +++ b/docs/modules.html @@ -7,7 +7,7 @@
  • maxTile: ITile

    corner tile for bbox with maximal x,y values

  • Returns BBox2d

    • bboxToTileRange(bbox: BBox2d, zoom: number): ITileRange
    • -

      coverts bbox to covering tile range of specified zoom level

      +

      converts bbox to covering tile range of specified zoom level

      Parameters

      • bbox: BBox2d
      • zoom: number

        target zoom level

      Returns ITileRange

      covering tile range

      @@ -86,7 +86,7 @@

      optional - default is 0 - if minResolutionDeg property was provided, the param will be ignored

    Returns number

    tile count included on provided feature and zooms ranges

    • -

      coverts tile coordinates between ll and ul

      +

      converts tile coordinates between ll and ul

      Parameters

      Returns ITile

      converted tile

      diff --git a/src/geo/bboxUtils.ts b/src/geo/bboxUtils.ts index 5fd2fec..2649e3e 100644 --- a/src/geo/bboxUtils.ts +++ b/src/geo/bboxUtils.ts @@ -66,7 +66,7 @@ export const bboxFromTiles = (minTile: ITile, maxTile: ITile): BBox2d => { }; /** - * coverts bbox to covering tile range of specified zoom level + * converts bbox to covering tile range of specified zoom level * @param bbox * @param zoom target zoom level * @returns covering tile range diff --git a/src/geo/tileBatcher.ts b/src/geo/tileBatcher.ts index 77472b2..9c7d58b 100644 --- a/src/geo/tileBatcher.ts +++ b/src/geo/tileBatcher.ts @@ -10,18 +10,18 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator maxX means sentinel (no partial row) + // reminderX > maxX means (no partial row) const remaining = range.maxX - reminderX + 1; // +1: maxX is inclusive if (remaining > requiredForFullBatch) { targetRanges.push({ @@ -45,12 +45,11 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator 0) { @@ -58,7 +57,7 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator= range.maxX) { range.minY++; } diff --git a/src/geo/tiles.ts b/src/geo/tiles.ts index 6f64de0..9e3caaa 100644 --- a/src/geo/tiles.ts +++ b/src/geo/tiles.ts @@ -80,7 +80,7 @@ export function degreesPerPixel(zoomLevel: number): number { } /** - * coverts tile coordinates between ll and ul + * converts tile coordinates between ll and ul * @param tile source tile * @returns converted tile */ diff --git a/tests/unit/geo/bboxUtils.spec.ts b/tests/unit/geo/bboxUtils.spec.ts index 0221c3c..06b53c8 100644 --- a/tests/unit/geo/bboxUtils.spec.ts +++ b/tests/unit/geo/bboxUtils.spec.ts @@ -33,7 +33,7 @@ describe('bboxUtils', () => { }); describe('bboxToTileRange', () => { - it('coverts bbox to expected tile range (no rounding, single tile)', () => { + it('converts bbox to expected tile range (no rounding, single tile)', () => { const bbox = [0, 0, 45, 45] as BBox2d; const range = bboxToTileRange(bbox, 2); @@ -48,7 +48,7 @@ describe('bboxUtils', () => { expect(range).toEqual(expectedRange); }); - it('coverts bbox to expected tile range (no rounding)', () => { + it('converts bbox to expected tile range (no rounding)', () => { const bbox = [0, 0, 90, 45] as BBox2d; const range = bboxToTileRange(bbox, 2); @@ -63,7 +63,7 @@ describe('bboxUtils', () => { expect(range).toEqual(expectedRange); }); - it('coverts bbox to expected tile range (rounding down)', () => { + it('converts bbox to expected tile range (rounding down)', () => { const bbox = [0, 0, 45, 45] as BBox2d; const range = bboxToTileRange(bbox, 1); @@ -78,7 +78,7 @@ describe('bboxUtils', () => { expect(range).toEqual(expectedRange); }); - it('coverts bbox to expected tile range (rounding up)', () => { + it('converts bbox to expected tile range (rounding up)', () => { const bbox = [0, 0, 45, 45.1] as BBox2d; const range = bboxToTileRange(bbox, 3); diff --git a/tests/unit/geo/tileBatcher.spec.ts b/tests/unit/geo/tileBatcher.spec.ts index e8f9d5e..107fddb 100644 --- a/tests/unit/geo/tileBatcher.spec.ts +++ b/tests/unit/geo/tileBatcher.spec.ts @@ -98,7 +98,7 @@ describe('GeoHashBatcher', () => { expect(batches).toEqual(expectedBatches); }); - it('return proper tiles for batch size that is power of 2', async function () { + it('yields each tile individually when batch size is 1', async function () { const ranges = [{ minX: 0, minY: 16, maxX: 15, maxY: 31, zoom: 5 }]; const rangeAsyncGen = (async function* () { yield await Promise.resolve(ranges[0]); diff --git a/tests/unit/geo/tiles.spec.ts b/tests/unit/geo/tiles.spec.ts index fe0454a..4f8d4ce 100644 --- a/tests/unit/geo/tiles.spec.ts +++ b/tests/unit/geo/tiles.spec.ts @@ -113,15 +113,16 @@ describe('tiles', () => { describe('tileRangeToTilesCount', () => { it('Check calculation for area calculation - tiles count by tiles range', function () { + // zoom 2: full-world grid is 8 columns (x: 0-7) x 4 rows (y: 0-3) = 32 tiles const batch: ITileRange = { - maxX: 180, - minX: -180, - maxY: 90, - minY: -90, - zoom: 0, + minX: 0, + maxX: 7, + minY: 0, + maxY: 3, + zoom: 2, }; const areaResult = tileRangeSize(batch); - const expectedResult = 65341; + const expectedResult = 32; expect(areaResult).toEqual(expectedResult); }); From d17b69b9de7a41b45ace82d097f02eda6edafb99 Mon Sep 17 00:00:00 2001 From: razbroc Date: Mon, 18 May 2026 14:47:05 +0300 Subject: [PATCH 7/7] fix: rename tileRangeSize to tileRangeToTilesCount for clarity and update related tests Co-authored-by: Copilot --- src/geo/tileBatcher.ts | 5 ++--- src/geo/tiles.ts | 6 +++--- tests/unit/geo/tileRanger.spec.ts | 2 +- tests/unit/geo/tiles.spec.ts | 5 ++--- 4 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/geo/tileBatcher.ts b/src/geo/tileBatcher.ts index 9c7d58b..acb62ba 100644 --- a/src/geo/tileBatcher.ts +++ b/src/geo/tileBatcher.ts @@ -18,11 +18,10 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator maxX means (no partial row) - const remaining = range.maxX - reminderX + 1; // +1: maxX is inclusive + const remaining = range.maxX - reminderX + 1; if (remaining > requiredForFullBatch) { targetRanges.push({ minX: reminderX, @@ -74,7 +73,7 @@ async function* tileBatchGenerator(batchSize: number, ranges: AsyncGenerator= range.maxX) { range.minY++; diff --git a/src/geo/tiles.ts b/src/geo/tiles.ts index 9e3caaa..64a4813 100644 --- a/src/geo/tiles.ts +++ b/src/geo/tiles.ts @@ -142,8 +142,8 @@ export function tileToBbox(tile: ITile): BBox2d { * @param ITileRange * @returns */ -export function tileRangeSize(batch: ITileRange): number { - return (batch.maxX - batch.minX + 1) * (batch.maxY - batch.minY + 1); // +1 on each axis because maxX/maxY are inclusive +export function tileRangeToTilesCount(tileRange: ITileRange): number { + return (tileRange.maxX - tileRange.minX + 1) * (tileRange.maxY - tileRange.minY + 1); } /** @@ -182,7 +182,7 @@ export function featureToTilesCount(feature: Feature, de for (let i = targetMinZoom; i <= targetMaxZoom; i++) { const zoomTilesBatch = bboxToTileRange(sanitized, i); - tilesTotalAmount += tileRangeSize(zoomTilesBatch); + tilesTotalAmount += tileRangeToTilesCount(zoomTilesBatch); } return tilesTotalAmount; diff --git a/tests/unit/geo/tileRanger.spec.ts b/tests/unit/geo/tileRanger.spec.ts index cc4058e..8aba55d 100644 --- a/tests/unit/geo/tileRanger.spec.ts +++ b/tests/unit/geo/tileRanger.spec.ts @@ -91,7 +91,7 @@ describe('TileRanger', () => { expect(ranges).toEqual(expectedRanges); }); - it('encodes none bbox polygon properly', async () => { + it('encodes non-bbox polygon properly', async () => { const poly = polygon([ [ [-45, 0], diff --git a/tests/unit/geo/tiles.spec.ts b/tests/unit/geo/tiles.spec.ts index 4f8d4ce..4167c96 100644 --- a/tests/unit/geo/tiles.spec.ts +++ b/tests/unit/geo/tiles.spec.ts @@ -1,7 +1,7 @@ import type { Feature, MultiPolygon, Polygon } from 'geojson'; import { ITileRange } from '../../../src'; import { - tileRangeSize, + tileRangeToTilesCount, degreesPerPixel, degreesPerPixelToZoomLevel, degreesPerTile, @@ -113,7 +113,6 @@ describe('tiles', () => { describe('tileRangeToTilesCount', () => { it('Check calculation for area calculation - tiles count by tiles range', function () { - // zoom 2: full-world grid is 8 columns (x: 0-7) x 4 rows (y: 0-3) = 32 tiles const batch: ITileRange = { minX: 0, maxX: 7, @@ -121,7 +120,7 @@ describe('tiles', () => { maxY: 3, zoom: 2, }; - const areaResult = tileRangeSize(batch); + const areaResult = tileRangeToTilesCount(batch); const expectedResult = 32; expect(areaResult).toEqual(expectedResult); });