@truelies/osm-dybuf 0.4.3 → 0.4.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -92,13 +92,17 @@ import {
92
92
  GridUnit,
93
93
  Gridex,
94
94
  INT_COORD_SCALE,
95
- GRID16_LEVEL,
96
- GRID16_LON_MAG_BITS,
97
- GRID16_LAT_MAG_BITS,
98
- grid16CodesFromLonLat,
99
- buildGrid16LonLatRangeForLevelCell,
100
- encodeSignedGridIndex,
101
- decodeSignedGridIndex,
95
+ CANONICAL_CODE_LEVEL,
96
+ gridexAtLonLat,
97
+ gridCodeAtLonLat,
98
+ gridCodeComponentsAtLonLat,
99
+ lonCodeFromLondex,
100
+ latCodeFromLatdex,
101
+ londexPrefixCodeFromLonCode,
102
+ lonLatCodeRangeForGridex,
103
+ encodeAxisCode,
104
+ decodeAxisCode,
105
+ lonCodeMagnitudeBits,
102
106
  } from "@truelies/osm-dybuf/grid_index";
103
107
 
104
108
  const unit = new GridUnit(8); // level 8
@@ -109,17 +113,27 @@ const [minLon, minLat, maxLon, maxLat] = unit.boundsOfGrid(cell);
109
113
  const children = unit.toLowerLevelGridexes(cell); // level 9, 4 cells
110
114
  const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level 8
111
115
 
112
- // canonical lv16 codes for Firestore / Redis
113
- const codes = grid16CodesFromLonLat(121.56, 25.03);
114
- console.log(codes.grid16LonCode, codes.grid16LatCode);
115
-
116
- // query a lv15 cell by expanding it into a lv16 numeric range
117
- const range = buildGrid16LonLatRangeForLevelCell(15, -3, 4);
118
- console.log(range.lon.from, range.lon.to, range.lat.from, range.lat.to);
119
-
120
- // signed code roundtrip
121
- const lonCode = encodeSignedGridIndex(-123, GRID16_LON_MAG_BITS);
122
- console.log(decodeSignedGridIndex(lonCode, GRID16_LON_MAG_BITS)); // -123
116
+ // GPS -> gridex / lonCode / latCode / gridCode
117
+ const gridex = gridexAtLonLat(16, 121.56, 25.03);
118
+ const parts = gridCodeComponentsAtLonLat(16, 121.56, 25.03);
119
+ console.log(gridex.londex, gridex.latdex);
120
+ console.log(parts.lonCode, parts.latCode, parts.gridCode);
121
+
122
+ // gridex <-> code
123
+ const lonCode = lonCodeFromLondex(16, -123);
124
+ const latCode = latCodeFromLatdex(16, 456);
125
+ console.log(lonCode, latCode);
126
+ console.log(gridCodeAtLonLat(16, 121.56, 25.03));
127
+
128
+ // canonical prefix / range for coarser level query
129
+ const lonPrefix = londexPrefixCodeFromLonCode(15, lonCode, CANONICAL_CODE_LEVEL);
130
+ const range = lonLatCodeRangeForGridex(15, -3, 4, CANONICAL_CODE_LEVEL);
131
+ console.log(lonPrefix, range.lonCode.from, range.lonCode.to);
132
+
133
+ // axis code roundtrip
134
+ const lonBits = lonCodeMagnitudeBits(CANONICAL_CODE_LEVEL);
135
+ const axisCode = encodeAxisCode(-123, lonBits);
136
+ console.log(decodeAxisCode(axisCode, lonBits)); // -123
123
137
  ```
124
138
 
125
139
  - `toLowerLevelGridexes(gridex)`:
@@ -128,15 +142,20 @@ console.log(decodeSignedGridIndex(lonCode, GRID16_LON_MAG_BITS)); // -123
128
142
  - `toUpperLevelGridex(gridex)`:
129
143
  - returns the parent cell at `level - 1`
130
144
  - throws when current unit is level `0`
131
- - `grid16CodesFromLonLat(lon, lat)`:
132
- - converts GPS directly to canonical `lv16` codes
133
- - returns `{ grid16LonCode, grid16LatCode, londex16, latdex16 }`
134
- - `buildGrid16LonLatRangeForLevelCell(level, londex, latdex)`:
135
- - converts any `lv <= 16` cell into the corresponding `lv16` prefix range
136
- - intended for Firestore numeric range query / Redis derived bucket lookup
137
- - `encodeSignedGridIndex(index, magnitudeBits)` / `decodeSignedGridIndex(code, magnitudeBits)`:
145
+ - `gridexAtLonLat(level, lon, lat)`:
146
+ - converts GPS directly to a `Gridex`
147
+ - `gridCodeComponentsAtLonLat(level, lon, lat)`:
148
+ - returns `{ gridex, lonCode, latCode, gridCode }`
149
+ - `lonCodeFromLondex(level, londex)` / `latCodeFromLatdex(level, latdex)`:
150
+ - converts axis indices to canonical axis codes
151
+ - `londexPrefixCodeFromLonCode(targetLevel, lonCode, sourceLevel?)`:
152
+ - trims a finer `lonCode` into a coarser-level prefix view
153
+ - `lonLatCodeRangeForGridex(level, londex, latdex, sourceLevel?)`:
154
+ - expands any `lv <= sourceLevel` cell into the corresponding canonical axis-code range
155
+ - `gridCodeAtLonLat(level, lon, lat)` / `gridCodeFromGridex(level, gridex)`:
156
+ - returns the packed `gridCode`
157
+ - `encodeAxisCode(index, magnitudeBits)` / `decodeAxisCode(code, magnitudeBits)`:
138
158
  - encodes grid index as `sign bit + (abs(index) - 1)`
139
- - for `lv16`, use `GRID16_LON_MAG_BITS = 19` and `GRID16_LAT_MAG_BITS = 18`
140
159
 
141
160
  ## Local Development
142
161
 
@@ -150,9 +169,9 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
150
169
  - `gridexesAt` near axis (no zero index cell)
151
170
  - `toLowerLevelGridexes` and `toUpperLevelGridex` roundtrip
152
171
  - level boundary errors (`lv0` has no upper level, `lv16` has no lower level)
153
- - canonical signed grid index encode/decode roundtrip
154
- - GPS -> `lv16` code conversion
155
- - `lv15` cell -> `lv16` range expansion correctness
172
+ - canonical axis-code encode/decode roundtrip
173
+ - GPS -> `gridex / lonCode / latCode / gridCode`
174
+ - `lv15` cell -> canonical code range expansion correctness
156
175
 
157
176
  - `npm pack`: build a tarball so other repos can install via `npm install ./path/to/osm-dybuf-*.tgz`
158
177
  - `npm link`: link the package locally (`npm link` here, then `npm link @truelies/osm-dybuf` in the consumer repo)
@@ -174,4 +193,4 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
174
193
  - `region_numeric_codes.json`: latest region ID table (generated via `scripts/dump_region_codes.py`)
175
194
  - `inspect_dybuf.mjs`: CLI tool that uses the package locally
176
195
 
177
- When schema IDs or DyBuf layouts change in the exporter, remember to bump the version here and re-publish/install so frontends and tools stay in sync. Version `0.4.3` aligns the package with the current `lv0~13` export range, the current frontend feature set (`water_wetland`, `building`, derived `label_*`, no exported `place_label`), the numeric `featureId` parser contract, and the new `lv16` canonical grid code helpers for Firestore / Redis indexing.
196
+ When schema IDs or DyBuf layouts change in the exporter, remember to bump the version here and re-publish/install so frontends and tools stay in sync. Version `0.4.3` aligns the package with the current `lv0~13` export range, the current frontend feature set (`water_wetland`, `building`, derived `label_*`, no exported `place_label`), the numeric `featureId` parser contract, and the canonical grid conversion helpers shared by Firestore / Redis / frontend indexing.
package/grid_index.d.ts CHANGED
@@ -2,9 +2,7 @@ export declare const INT_COORD_SCALE: number;
2
2
  export declare const GRID_FINE_RES: number;
3
3
  export declare const GRID_LV0_UNIT: number;
4
4
  export declare const MAX_LEVEL: number;
5
- export declare const GRID16_LEVEL: number;
6
- export declare const GRID16_LON_MAG_BITS: number;
7
- export declare const GRID16_LAT_MAG_BITS: number;
5
+ export declare const CANONICAL_CODE_LEVEL: number;
8
6
  export declare class Gridex {
9
7
  readonly londex: number;
10
8
  readonly latdex: number;
@@ -53,36 +51,65 @@ export declare class GridUnit {
53
51
  }
54
52
  export declare function gridexAtLonLat(level: number, longitude: number, latitude: number): GridexAtLv;
55
53
  export declare function gridexStringAtLonLat(level: number, longitude: number, latitude: number): string;
56
- export declare function encodeSignedGridIndex(index: number, magnitudeBits: number): number;
57
- export declare function decodeSignedGridIndex(code: number, magnitudeBits: number): number;
58
- export declare function grid16CodesFromLonLat(
59
- longitude: number,
60
- latitude: number
54
+ export declare function lonCodeMagnitudeBits(level: number): number;
55
+ export declare function latCodeMagnitudeBits(level: number): number;
56
+ export declare function encodeAxisCode(index: number, magnitudeBits: number): number;
57
+ export declare function decodeAxisCode(code: number, magnitudeBits: number): number;
58
+ export declare function lonCodeFromLondex(level: number, londex: number): number;
59
+ export declare function latCodeFromLatdex(level: number, latdex: number): number;
60
+ export declare function londexFromLonCode(level: number, lonCode: number): number;
61
+ export declare function latdexFromLatCode(level: number, latCode: number): number;
62
+ export declare function londexPrefixCodeFromLonCode(targetLevel: number, lonCode: number, sourceLevel?: number): number;
63
+ export declare function latdexPrefixCodeFromLatCode(targetLevel: number, latCode: number, sourceLevel?: number): number;
64
+ export declare function lonCodeRangeForLondex(
65
+ level: number,
66
+ londex: number,
67
+ sourceLevel?: number
61
68
  ): {
62
- grid16LonCode: number;
63
- grid16LatCode: number;
64
- londex16: number;
65
- latdex16: number;
69
+ from: number;
70
+ to: number;
66
71
  };
67
- export declare function buildGrid16RangeForLevelIndex(
72
+ export declare function latCodeRangeForLatdex(
68
73
  level: number,
69
- index: number,
70
- magnitudeBits: number
74
+ latdex: number,
75
+ sourceLevel?: number
71
76
  ): {
72
77
  from: number;
73
78
  to: number;
74
79
  };
75
- export declare function buildGrid16LonLatRangeForLevelCell(
80
+ export declare function lonLatCodeRangeForGridex(
76
81
  level: number,
77
82
  londex: number,
78
- latdex: number
83
+ latdex: number,
84
+ sourceLevel?: number
79
85
  ): {
80
- lon: {
86
+ lonCode: {
81
87
  from: number;
82
88
  to: number;
83
89
  };
84
- lat: {
90
+ latCode: {
85
91
  from: number;
86
92
  to: number;
87
93
  };
88
94
  };
95
+ export declare function gridCodeFromLonLatCodes(level: number, lonCode: number, latCode: number): number;
96
+ export declare function lonLatCodesFromGridCode(
97
+ level: number,
98
+ gridCode: number
99
+ ): {
100
+ lonCode: number;
101
+ latCode: number;
102
+ };
103
+ export declare function gridCodeFromGridex(level: number, gridex: Gridex | number, latdex?: number): number;
104
+ export declare function gridexFromGridCode(level: number, gridCode: number): GridexAtLv;
105
+ export declare function gridCodeAtLonLat(level: number, longitude: number, latitude: number): number;
106
+ export declare function gridCodeComponentsAtLonLat(
107
+ level: number,
108
+ longitude: number,
109
+ latitude: number
110
+ ): {
111
+ gridex: GridexAtLv;
112
+ lonCode: number;
113
+ latCode: number;
114
+ gridCode: number;
115
+ };
package/grid_index.mjs CHANGED
@@ -11,9 +11,7 @@ const MAX_LONGITUDE_FINE = 180 * INT_COORD_SCALE * GRID_FINE_RES;
11
11
  const MIN_LONGITUDE_FINE = -MAX_LONGITUDE_FINE;
12
12
  const MAX_LATITUDE_FINE = 90 * INT_COORD_SCALE * GRID_FINE_RES;
13
13
  const MIN_LATITUDE_FINE = -MAX_LATITUDE_FINE;
14
- export const GRID16_LEVEL = 16;
15
- export const GRID16_LON_MAG_BITS = 19;
16
- export const GRID16_LAT_MAG_BITS = 18;
14
+ export const CANONICAL_CODE_LEVEL = MAX_LEVEL;
17
15
 
18
16
  export class Gridex {
19
17
  constructor(londex, latdex) {
@@ -266,7 +264,17 @@ export function gridexStringAtLonLat(level, longitude, latitude) {
266
264
  return `${gridex.londex},${gridex.latdex}`;
267
265
  }
268
266
 
269
- export function encodeSignedGridIndex(index, magnitudeBits) {
267
+ export function lonCodeMagnitudeBits(level) {
268
+ const unit = new GridUnit(level);
269
+ return Math.ceil(Math.log2(unit.maxLondex));
270
+ }
271
+
272
+ export function latCodeMagnitudeBits(level) {
273
+ const unit = new GridUnit(level);
274
+ return Math.ceil(Math.log2(unit.maxLatdex));
275
+ }
276
+
277
+ export function encodeAxisCode(index, magnitudeBits) {
270
278
  if (!Number.isInteger(index) || index === 0) {
271
279
  throw new Error(`Grid index must be a non-zero integer: ${index}`);
272
280
  }
@@ -279,7 +287,7 @@ export function encodeSignedGridIndex(index, magnitudeBits) {
279
287
  return (signBit << magnitudeBits) | magnitude;
280
288
  }
281
289
 
282
- export function decodeSignedGridIndex(code, magnitudeBits) {
290
+ export function decodeAxisCode(code, magnitudeBits) {
283
291
  if (!Number.isInteger(code) || code < 0) {
284
292
  throw new Error(`Grid code must be a non-negative integer: ${code}`);
285
293
  }
@@ -289,41 +297,142 @@ export function decodeSignedGridIndex(code, magnitudeBits) {
289
297
  return (magnitude + 1) * (signBit === 1 ? 1 : -1);
290
298
  }
291
299
 
292
- export function grid16CodesFromLonLat(longitude, latitude) {
293
- const gridex16 = gridexAtLonLat(GRID16_LEVEL, longitude, latitude);
294
- return {
295
- grid16LonCode: encodeSignedGridIndex(gridex16.londex, GRID16_LON_MAG_BITS),
296
- grid16LatCode: encodeSignedGridIndex(gridex16.latdex, GRID16_LAT_MAG_BITS),
297
- londex16: gridex16.londex,
298
- latdex16: gridex16.latdex,
299
- };
300
+ export function lonCodeFromLondex(level, londex) {
301
+ return encodeAxisCode(londex, lonCodeMagnitudeBits(level));
302
+ }
303
+
304
+ export function latCodeFromLatdex(level, latdex) {
305
+ return encodeAxisCode(latdex, latCodeMagnitudeBits(level));
300
306
  }
301
307
 
302
- export function buildGrid16RangeForLevelIndex(level, index, magnitudeBits) {
303
- if (!Number.isInteger(level) || level < 0 || level > GRID16_LEVEL) {
304
- throw new Error(`Level must be an integer between 0 and ${GRID16_LEVEL}: ${level}`);
308
+ export function londexFromLonCode(level, lonCode) {
309
+ return decodeAxisCode(lonCode, lonCodeMagnitudeBits(level));
310
+ }
311
+
312
+ export function latdexFromLatCode(level, latCode) {
313
+ return decodeAxisCode(latCode, latCodeMagnitudeBits(level));
314
+ }
315
+
316
+ function axisCodePrefixFromCode(targetLevel, axisCode, sourceLevel, magnitudeBits) {
317
+ if (!Number.isInteger(targetLevel) || targetLevel < 0 || targetLevel > sourceLevel) {
318
+ throw new Error(`targetLevel must be an integer between 0 and ${sourceLevel}: ${targetLevel}`);
319
+ }
320
+ const signBase = 2 ** magnitudeBits;
321
+ const signBit = Math.floor(axisCode / signBase);
322
+ const magnitude = axisCode % signBase;
323
+ const shift = sourceLevel - targetLevel;
324
+ const prefixMagnitude = shift === 0 ? magnitude : Math.floor(magnitude / (2 ** shift)) * (2 ** shift);
325
+ return signBit * signBase + prefixMagnitude;
326
+ }
327
+
328
+ export function londexPrefixCodeFromLonCode(targetLevel, lonCode, sourceLevel = CANONICAL_CODE_LEVEL) {
329
+ return axisCodePrefixFromCode(targetLevel, lonCode, sourceLevel, lonCodeMagnitudeBits(sourceLevel));
330
+ }
331
+
332
+ export function latdexPrefixCodeFromLatCode(targetLevel, latCode, sourceLevel = CANONICAL_CODE_LEVEL) {
333
+ return axisCodePrefixFromCode(targetLevel, latCode, sourceLevel, latCodeMagnitudeBits(sourceLevel));
334
+ }
335
+
336
+ function axisCodeRangeForIndex(targetLevel, index, sourceLevel, magnitudeBits) {
337
+ if (!Number.isInteger(sourceLevel) || sourceLevel < 0 || sourceLevel > MAX_LEVEL) {
338
+ throw new Error(`sourceLevel must be an integer between 0 and ${MAX_LEVEL}: ${sourceLevel}`);
339
+ }
340
+ if (!Number.isInteger(targetLevel) || targetLevel < 0 || targetLevel > sourceLevel) {
341
+ throw new Error(`targetLevel must be an integer between 0 and ${sourceLevel}: ${targetLevel}`);
305
342
  }
306
343
  if (!Number.isInteger(index) || index === 0) {
307
344
  throw new Error(`Grid index must be a non-zero integer: ${index}`);
308
345
  }
309
346
  const signBit = index > 0 ? 1 : 0;
310
- const shift = GRID16_LEVEL - level;
347
+ const shift = sourceLevel - targetLevel;
311
348
  const magnitude = Math.abs(index) - 1;
312
349
  const fromMagnitude = magnitude << shift;
313
350
  const toMagnitude = ((magnitude + 1) << shift) - 1;
314
- const maxMagnitude = (1 << magnitudeBits) - 1;
351
+ const maxMagnitude = (2 ** magnitudeBits) - 1;
315
352
  if (toMagnitude > maxMagnitude) {
316
- throw new Error(`Grid index ${index} at level ${level} exceeds lv16 range`);
353
+ throw new Error(`Grid index ${index} at level ${targetLevel} exceeds source level ${sourceLevel} range`);
354
+ }
355
+ const signBase = 2 ** magnitudeBits;
356
+ return {
357
+ from: signBit * signBase + fromMagnitude,
358
+ to: signBit * signBase + toMagnitude,
359
+ };
360
+ }
361
+
362
+ export function lonCodeRangeForLondex(level, londex, sourceLevel = CANONICAL_CODE_LEVEL) {
363
+ return axisCodeRangeForIndex(level, londex, sourceLevel, lonCodeMagnitudeBits(sourceLevel));
364
+ }
365
+
366
+ export function latCodeRangeForLatdex(level, latdex, sourceLevel = CANONICAL_CODE_LEVEL) {
367
+ return axisCodeRangeForIndex(level, latdex, sourceLevel, latCodeMagnitudeBits(sourceLevel));
368
+ }
369
+
370
+ export function lonLatCodeRangeForGridex(level, londex, latdex, sourceLevel = CANONICAL_CODE_LEVEL) {
371
+ return {
372
+ lonCode: lonCodeRangeForLondex(level, londex, sourceLevel),
373
+ latCode: latCodeRangeForLatdex(level, latdex, sourceLevel),
374
+ };
375
+ }
376
+
377
+ export function gridCodeFromLonLatCodes(level, lonCode, latCode) {
378
+ const latCodeWidth = latCodeMagnitudeBits(level) + 1;
379
+ const latCodeSpan = 2 ** latCodeWidth;
380
+ if (!Number.isInteger(lonCode) || lonCode < 0) {
381
+ throw new Error(`lonCode must be a non-negative integer: ${lonCode}`);
382
+ }
383
+ if (!Number.isInteger(latCode) || latCode < 0 || latCode >= latCodeSpan) {
384
+ throw new Error(`latCode out of range for level ${level}: ${latCode}`);
317
385
  }
386
+ return lonCode * latCodeSpan + latCode;
387
+ }
388
+
389
+ export function lonLatCodesFromGridCode(level, gridCode) {
390
+ if (!Number.isInteger(gridCode) || gridCode < 0) {
391
+ throw new Error(`gridCode must be a non-negative integer: ${gridCode}`);
392
+ }
393
+ const latCodeWidth = latCodeMagnitudeBits(level) + 1;
394
+ const latCodeSpan = 2 ** latCodeWidth;
318
395
  return {
319
- from: (signBit << magnitudeBits) | fromMagnitude,
320
- to: (signBit << magnitudeBits) | toMagnitude,
396
+ lonCode: Math.floor(gridCode / latCodeSpan),
397
+ latCode: gridCode % latCodeSpan,
321
398
  };
322
399
  }
323
400
 
324
- export function buildGrid16LonLatRangeForLevelCell(level, londex, latdex) {
401
+ export function gridCodeFromGridex(level, gridex, latdex) {
402
+ const londex = typeof gridex === "object" ? gridex.londex : gridex;
403
+ const resolvedLatdex = typeof gridex === "object" ? gridex.latdex : latdex;
404
+ if (!Number.isInteger(londex) || !Number.isInteger(resolvedLatdex)) {
405
+ throw new Error("gridCodeFromGridex expects londex/latdex integers or a Gridex-like object");
406
+ }
407
+ return gridCodeFromLonLatCodes(
408
+ level,
409
+ lonCodeFromLondex(level, londex),
410
+ latCodeFromLatdex(level, resolvedLatdex),
411
+ );
412
+ }
413
+
414
+ export function gridexFromGridCode(level, gridCode) {
415
+ const { lonCode, latCode } = lonLatCodesFromGridCode(level, gridCode);
416
+ const unit = new GridUnit(level);
417
+ return unit.makeGridex(
418
+ londexFromLonCode(level, lonCode),
419
+ latdexFromLatCode(level, latCode),
420
+ );
421
+ }
422
+
423
+ export function gridCodeAtLonLat(level, longitude, latitude) {
424
+ const gridex = gridexAtLonLat(level, longitude, latitude);
425
+ return gridCodeFromGridex(level, gridex);
426
+ }
427
+
428
+ export function gridCodeComponentsAtLonLat(level, longitude, latitude) {
429
+ const gridex = gridexAtLonLat(level, longitude, latitude);
430
+ const lonCode = lonCodeFromLondex(level, gridex.londex);
431
+ const latCode = latCodeFromLatdex(level, gridex.latdex);
325
432
  return {
326
- lon: buildGrid16RangeForLevelIndex(level, londex, GRID16_LON_MAG_BITS),
327
- lat: buildGrid16RangeForLevelIndex(level, latdex, GRID16_LAT_MAG_BITS),
433
+ gridex,
434
+ lonCode,
435
+ latCode,
436
+ gridCode: gridCodeFromLonLatCodes(level, lonCode, latCode),
328
437
  };
329
438
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truelies/osm-dybuf",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "Gridex OSM DyBuf parser and schema utilities",
5
5
  "type": "module",
6
6
  "main": "./osm_dybuf.mjs",