@truelies/osm-dybuf 0.4.2 → 0.4.3

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
@@ -88,7 +88,18 @@ Feature ID usage notes:
88
88
  `grid_index.mjs` exposes Gridex helpers (integers, pole triangles) aligned with the exporter:
89
89
 
90
90
  ```ts
91
- import { GridUnit, Gridex, INT_COORD_SCALE } from "@truelies/osm-dybuf/grid_index";
91
+ import {
92
+ GridUnit,
93
+ Gridex,
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,
102
+ } from "@truelies/osm-dybuf/grid_index";
92
103
 
93
104
  const unit = new GridUnit(8); // level 8
94
105
  const cell = new Gridex(213, 161);
@@ -97,6 +108,18 @@ const [minLon, minLat, maxLon, maxLat] = unit.boundsOfGrid(cell);
97
108
  // parent/child conversion (same rules as Python GridUnit)
98
109
  const children = unit.toLowerLevelGridexes(cell); // level 9, 4 cells
99
110
  const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level 8
111
+
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
100
123
  ```
101
124
 
102
125
  - `toLowerLevelGridexes(gridex)`:
@@ -105,6 +128,15 @@ const parent = new GridUnit(9).toUpperLevelGridex(children[0]); // back to level
105
128
  - `toUpperLevelGridex(gridex)`:
106
129
  - returns the parent cell at `level - 1`
107
130
  - 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)`:
138
+ - encodes grid index as `sign bit + (abs(index) - 1)`
139
+ - for `lv16`, use `GRID16_LON_MAG_BITS = 19` and `GRID16_LAT_MAG_BITS = 18`
108
140
 
109
141
  ## Local Development
110
142
 
@@ -118,6 +150,9 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
118
150
  - `gridexesAt` near axis (no zero index cell)
119
151
  - `toLowerLevelGridexes` and `toUpperLevelGridex` roundtrip
120
152
  - 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
121
156
 
122
157
  - `npm pack`: build a tarball so other repos can install via `npm install ./path/to/osm-dybuf-*.tgz`
123
158
  - `npm link`: link the package locally (`npm link` here, then `npm link @truelies/osm-dybuf` in the consumer repo)
@@ -139,4 +174,4 @@ Unit test example (`tests/grid_index.test.mjs`) covers:
139
174
  - `region_numeric_codes.json`: latest region ID table (generated via `scripts/dump_region_codes.py`)
140
175
  - `inspect_dybuf.mjs`: CLI tool that uses the package locally
141
176
 
142
- 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.2` aligns the package with the current `lv0~13` export range, the current frontend feature set (`water_wetland`, `building`, derived `label_*`, no exported `place_label`), and the numeric `featureId` parser contract.
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.
package/grid_index.d.ts CHANGED
@@ -1,6 +1,10 @@
1
1
  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
+ 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;
4
8
  export declare class Gridex {
5
9
  readonly londex: number;
6
10
  readonly latdex: number;
@@ -47,3 +51,38 @@ export declare class GridUnit {
47
51
  maxLatdex: number;
48
52
  };
49
53
  }
54
+ export declare function gridexAtLonLat(level: number, longitude: number, latitude: number): GridexAtLv;
55
+ 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
61
+ ): {
62
+ grid16LonCode: number;
63
+ grid16LatCode: number;
64
+ londex16: number;
65
+ latdex16: number;
66
+ };
67
+ export declare function buildGrid16RangeForLevelIndex(
68
+ level: number,
69
+ index: number,
70
+ magnitudeBits: number
71
+ ): {
72
+ from: number;
73
+ to: number;
74
+ };
75
+ export declare function buildGrid16LonLatRangeForLevelCell(
76
+ level: number,
77
+ londex: number,
78
+ latdex: number
79
+ ): {
80
+ lon: {
81
+ from: number;
82
+ to: number;
83
+ };
84
+ lat: {
85
+ from: number;
86
+ to: number;
87
+ };
88
+ };
package/grid_index.mjs CHANGED
@@ -6,11 +6,14 @@ export const GRID_LV0_UNIT = 30 * INT_COORD_SCALE;
6
6
  const GRID_LV0_UNIT_FINE = GRID_LV0_UNIT * GRID_FINE_RES;
7
7
  const GRID_LV0_LONDEX = 6;
8
8
  const GRID_LV0_LATDEX = 3;
9
- const MAX_LEVEL = 16;
9
+ export const MAX_LEVEL = 16;
10
10
  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
17
 
15
18
  export class Gridex {
16
19
  constructor(londex, latdex) {
@@ -230,3 +233,97 @@ export class GridUnit {
230
233
  return { minLondex, maxLondex, minLatdex, maxLatdex };
231
234
  }
232
235
  }
236
+
237
+ function wrapLongitudeDegrees(longitude) {
238
+ let value = longitude;
239
+ while (value > 180) value -= 360;
240
+ while (value < -180) value += 360;
241
+ return value;
242
+ }
243
+
244
+ function clampLatitudeDegrees(latitude) {
245
+ return Math.max(-90, Math.min(90, latitude));
246
+ }
247
+
248
+ function normalizeLonLatDegrees(longitude, latitude) {
249
+ return {
250
+ longitudeInt: Math.round(wrapLongitudeDegrees(longitude) * INT_COORD_SCALE),
251
+ latitudeInt: Math.round(clampLatitudeDegrees(latitude) * INT_COORD_SCALE),
252
+ };
253
+ }
254
+
255
+ export function gridexAtLonLat(level, longitude, latitude) {
256
+ const unit = new GridUnit(level);
257
+ const { longitudeInt, latitudeInt } = normalizeLonLatDegrees(longitude, latitude);
258
+ const candidates = unit.gridexesAt(longitudeInt, latitudeInt);
259
+ return candidates.find((candidate) => candidate.londex !== 0 && candidate.latdex !== 0)
260
+ ?? candidates[0]
261
+ ?? unit.makeGridex(0, 0);
262
+ }
263
+
264
+ export function gridexStringAtLonLat(level, longitude, latitude) {
265
+ const gridex = gridexAtLonLat(level, longitude, latitude);
266
+ return `${gridex.londex},${gridex.latdex}`;
267
+ }
268
+
269
+ export function encodeSignedGridIndex(index, magnitudeBits) {
270
+ if (!Number.isInteger(index) || index === 0) {
271
+ throw new Error(`Grid index must be a non-zero integer: ${index}`);
272
+ }
273
+ const signBit = index > 0 ? 1 : 0;
274
+ const magnitude = Math.abs(index) - 1;
275
+ const maxMagnitude = (1 << magnitudeBits) - 1;
276
+ if (magnitude < 0 || magnitude > maxMagnitude) {
277
+ throw new Error(`Grid index ${index} exceeds ${magnitudeBits} magnitude bits`);
278
+ }
279
+ return (signBit << magnitudeBits) | magnitude;
280
+ }
281
+
282
+ export function decodeSignedGridIndex(code, magnitudeBits) {
283
+ if (!Number.isInteger(code) || code < 0) {
284
+ throw new Error(`Grid code must be a non-negative integer: ${code}`);
285
+ }
286
+ const signBit = code >> magnitudeBits;
287
+ const magnitudeMask = (1 << magnitudeBits) - 1;
288
+ const magnitude = code & magnitudeMask;
289
+ return (magnitude + 1) * (signBit === 1 ? 1 : -1);
290
+ }
291
+
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
+ }
301
+
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}`);
305
+ }
306
+ if (!Number.isInteger(index) || index === 0) {
307
+ throw new Error(`Grid index must be a non-zero integer: ${index}`);
308
+ }
309
+ const signBit = index > 0 ? 1 : 0;
310
+ const shift = GRID16_LEVEL - level;
311
+ const magnitude = Math.abs(index) - 1;
312
+ const fromMagnitude = magnitude << shift;
313
+ const toMagnitude = ((magnitude + 1) << shift) - 1;
314
+ const maxMagnitude = (1 << magnitudeBits) - 1;
315
+ if (toMagnitude > maxMagnitude) {
316
+ throw new Error(`Grid index ${index} at level ${level} exceeds lv16 range`);
317
+ }
318
+ return {
319
+ from: (signBit << magnitudeBits) | fromMagnitude,
320
+ to: (signBit << magnitudeBits) | toMagnitude,
321
+ };
322
+ }
323
+
324
+ export function buildGrid16LonLatRangeForLevelCell(level, londex, latdex) {
325
+ return {
326
+ lon: buildGrid16RangeForLevelIndex(level, londex, GRID16_LON_MAG_BITS),
327
+ lat: buildGrid16RangeForLevelIndex(level, latdex, GRID16_LAT_MAG_BITS),
328
+ };
329
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truelies/osm-dybuf",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Gridex OSM DyBuf parser and schema utilities",
5
5
  "type": "module",
6
6
  "main": "./osm_dybuf.mjs",