@truelies/osm-dybuf 0.2.6 → 0.3.0

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
@@ -3,6 +3,19 @@
3
3
  Gridex `osm.dybuf` parser utilities shared by the exporter, inspection scripts, and Next.js frontend.
4
4
  This package wraps the shared schema tables (`schema_ids`) and exposes a high-level `parseOsmDybuf` helper.
5
5
  It depends on the published [`dybuf`](https://www.npmjs.com/package/dybuf) runtime (≥ 0.4.2).
6
+ Feature IDs are packed as `feature_id = sub_id * 64 + main_id` (both 0–63). Use `schema_ids` to decode feature strings.
7
+
8
+ ## Current Compatibility Status
9
+
10
+ `parseOsmDybuf` now supports the current cloud formats and keeps a legacy fallback.
11
+
12
+ Support matrix:
13
+
14
+ - Flat V1 cell file (`osm.dybuf`): `version + payload` -> supported (`format="flat_v1"`)
15
+ - V2 overview/bundle (`overview.dybuf`, `bundle.dybuf`): `version=2 + index entries` -> supported (`format="v2_indexed"`)
16
+ - Legacy V1 with region chunks: `version + [region_id + region_payload]...` -> supported as fallback (`format="legacy_v1_regions"`)
17
+
18
+ `inspect_dybuf.mjs` now prints by detected format.
6
19
 
7
20
  ## Usage
8
21
 
@@ -21,10 +34,25 @@ import {
21
34
  const data = await fetch("/tiles/08/213/161/osm.dybuf").then((res) => res.arrayBuffer());
22
35
  const cell = { level: 8, londex: 213, latdex: 161 };
23
36
  const parsed = parseOsmDybuf(data, cell);
37
+
38
+ if (parsed.format === "flat_v1") {
39
+ console.log(parsed.features.length);
40
+ } else if (parsed.format === "v2_indexed") {
41
+ console.log(parsed.entries.length);
42
+ } else {
43
+ console.log(parsed.regions.length);
44
+ }
24
45
  ```
25
46
 
26
47
  - CLI inspection (development only):
27
48
  ```bash
49
+ # Preferred: only provide file path.
50
+ # For v1 osm.dybuf, the tool auto-infers level/londex/latdex from .../<lv>/<x>/<y>/osm.dybuf
51
+ node js/inspect_dybuf.mjs \
52
+ --file /path/to/osm.dybuf \
53
+ --limit 5
54
+
55
+ # Optional fallback when path does not contain cell coordinates (needed for v1):
28
56
  node js/inspect_dybuf.mjs \
29
57
  --file /path/to/osm.dybuf \
30
58
  --level 8 --londex 213 --latdex 161 --limit 5
package/osm_dybuf.d.mts CHANGED
@@ -10,7 +10,7 @@ export interface GeometryCoordinate {
10
10
  }
11
11
 
12
12
  export interface GeometrySegment {
13
- role: string;
13
+ role: string | null;
14
14
  coordinates: GeometryCoordinate[];
15
15
  }
16
16
 
@@ -34,10 +34,36 @@ export interface RegionChunk {
34
34
  features: GridFeature[];
35
35
  }
36
36
 
37
- export interface OsmDybuf {
38
- version: number;
37
+ export interface V2IndexEntry {
38
+ cell: GridCellRef;
39
+ features: GridFeature[];
40
+ }
41
+
42
+ export interface FlatV1Dybuf {
43
+ version: 1;
44
+ format: "flat_v1";
45
+ cell: GridCellRef;
46
+ features: GridFeature[];
47
+ regions: RegionChunk[];
48
+ }
49
+
50
+ export interface LegacyV1Dybuf {
51
+ version: 1;
52
+ format: "legacy_v1_regions";
39
53
  regions: RegionChunk[];
40
54
  }
41
55
 
56
+ export interface V2Dybuf {
57
+ version: 2;
58
+ format: "v2_indexed";
59
+ entries: V2IndexEntry[];
60
+ }
61
+
62
+ export type OsmDybuf = FlatV1Dybuf | LegacyV1Dybuf | V2Dybuf | {
63
+ version: number;
64
+ format: string;
65
+ };
66
+
42
67
  export declare function parseOsmDybuf(data: ArrayBuffer | ArrayBufferView, cell: GridCellRef): OsmDybuf;
43
- export declare function decodeRegionPayload(payload: ArrayBuffer, cell: GridCellRef): GridFeature[];
68
+ export declare function decodePayload(payload: ArrayBuffer | ArrayBufferView, cell: GridCellRef): GridFeature[];
69
+ export declare function decodeRegionPayload(payload: ArrayBuffer | ArrayBufferView, cell: GridCellRef): GridFeature[];
package/osm_dybuf.mjs CHANGED
@@ -1,11 +1,7 @@
1
1
  import {
2
2
  DyBuf,
3
- TYPDEX_TYP_BOOL,
4
- TYPDEX_TYP_INT,
5
3
  TYPDEX_TYP_UINT,
6
- TYPDEX_TYP_STRING,
7
4
  TYPDEX_TYP_BYTES,
8
- TYPDEX_TYP_ARRAY,
9
5
  TYPDEX_TYP_MAP,
10
6
  } from "dybuf";
11
7
  import {
@@ -106,11 +102,11 @@ function decodeGeometrySegments(bytes, cell) {
106
102
  const segmentCount = readVarUint(buf);
107
103
  for (let i = 0; i < segmentCount; i += 1) {
108
104
  const roleId = buf.getByte();
109
- let role = "";
105
+ let role = null;
110
106
  if (roleId === SEGMENT_ROLE_ID_UNSUPPORTED) {
111
107
  role = "<unsupported>";
112
108
  } else if (roleId !== SEGMENT_ROLE_ID_UNSET) {
113
- role = ID_TO_SEGMENT_ROLE[String(roleId)] ?? "";
109
+ role = ID_TO_SEGMENT_ROLE[String(roleId)] ?? "<unsupported>";
114
110
  }
115
111
  const pointCount = readVarUint(buf);
116
112
  const points = [];
@@ -192,14 +188,14 @@ function decodeGridPayload(bytes, cell) {
192
188
  };
193
189
  }
194
190
 
195
- function decodeRegionPayload(payloadBytes, cell) {
191
+ function decodePayload(payloadBytes, cell) {
196
192
  const buf = new DyBuf(payloadBytes, false);
197
193
  const featureCount = readVarUint(buf);
198
194
  const features = [];
199
195
  for (let i = 0; i < featureCount; i += 1) {
200
196
  const featureNumericId = readVarUint(buf);
201
197
  const featureId =
202
- ID_TO_FEATURE_KIND[featureNumericId] ?? `feature:${featureNumericId}`;
198
+ ID_TO_FEATURE_KIND[String(featureNumericId)] ?? `feature:${featureNumericId}`;
203
199
  const entryCount = readVarUint(buf);
204
200
  const elements = [];
205
201
  for (let j = 0; j < entryCount; j += 1) {
@@ -208,9 +204,48 @@ function decodeRegionPayload(payloadBytes, cell) {
208
204
  }
209
205
  features.push({ featureId, elements });
210
206
  }
207
+ if (buf.position() !== buf.limit()) {
208
+ throw new Error(
209
+ `Payload has trailing bytes (${buf.limit() - buf.position()} bytes remain)`
210
+ );
211
+ }
211
212
  return features;
212
213
  }
213
214
 
215
+ function decodeLegacyRegions(bodyBytes, cell) {
216
+ const buf = new DyBuf(bodyBytes, false);
217
+ const regions = [];
218
+ while (buf.position() < buf.limit()) {
219
+ const regionId = readVarUint(buf);
220
+ const regionName = REGION_ID_TO_NAME[String(regionId)] ?? `region:${regionId}`;
221
+ const regionPayload = readVarBytes(buf);
222
+ const features = decodePayload(regionPayload, cell);
223
+ regions.push({ id: regionId, name: regionName, features });
224
+ }
225
+ return regions;
226
+ }
227
+
228
+ function decodeV2Entries(bodyBytes) {
229
+ const buf = new DyBuf(bodyBytes, false);
230
+ const count = readVarUint(buf);
231
+ const entries = [];
232
+ for (let i = 0; i < count; i += 1) {
233
+ const level = buf.getByte();
234
+ const londex = readVarInt(buf);
235
+ const latdex = readVarInt(buf);
236
+ const payloadBytes = readVarBytes(buf);
237
+ const cell = { level, londex, latdex };
238
+ const features = decodePayload(payloadBytes, cell);
239
+ entries.push({ cell, features });
240
+ }
241
+ if (buf.position() !== buf.limit()) {
242
+ throw new Error(
243
+ `V2 payload has trailing bytes (${buf.limit() - buf.position()} bytes remain)`
244
+ );
245
+ }
246
+ return entries;
247
+ }
248
+
214
249
  /**
215
250
  * Parse a single cell's osm.dybuf payload.
216
251
  * @param {ArrayBuffer | ArrayBufferView} data
@@ -221,17 +256,51 @@ export function parseOsmDybuf(data, cell) {
221
256
  throw new TypeError("Cell metadata (level, londex, latdex) is required");
222
257
  }
223
258
  const buffer = toArrayBuffer(data);
224
- const buf = new DyBuf(buffer, false);
225
- const version = buf.getByte();
226
- const regions = [];
227
- while (buf.position() < buf.limit()) {
228
- const regionId = readVarUint(buf);
229
- const regionName = REGION_ID_TO_NAME[String(regionId)] ?? `region:${regionId}`;
230
- const regionPayload = readVarBytes(buf);
231
- const features = decodeRegionPayload(regionPayload, cell);
232
- regions.push({ id: regionId, name: regionName, features });
259
+ const bytes = new Uint8Array(buffer);
260
+ if (bytes.length === 0) {
261
+ throw new Error("Empty dybuf payload");
262
+ }
263
+
264
+ const version = bytes[0];
265
+ const body = bytes.buffer.slice(bytes.byteOffset + 1, bytes.byteOffset + bytes.byteLength);
266
+
267
+ if (version === 2) {
268
+ return {
269
+ version,
270
+ format: "v2_indexed",
271
+ entries: decodeV2Entries(body),
272
+ };
273
+ }
274
+
275
+ if (version !== 1) {
276
+ throw new Error(`Unsupported dybuf version ${version}`);
277
+ }
278
+
279
+ // Prefer current flat V1: version + payload
280
+ try {
281
+ const features = decodePayload(body, cell);
282
+ return {
283
+ version,
284
+ format: "flat_v1",
285
+ cell: { level: cell.level, londex: cell.londex, latdex: cell.latdex },
286
+ features,
287
+ regions: [],
288
+ };
289
+ } catch (flatErr) {
290
+ // Fallback to legacy format: version + [region_id + region_payload]...
291
+ try {
292
+ const regions = decodeLegacyRegions(body, cell);
293
+ return {
294
+ version,
295
+ format: "legacy_v1_regions",
296
+ regions,
297
+ };
298
+ } catch (legacyErr) {
299
+ throw new Error(
300
+ `Unable to decode V1 dybuf as flat or legacy regions. flat=${String(flatErr)} legacy=${String(legacyErr)}`
301
+ );
302
+ }
233
303
  }
234
- return { version, regions };
235
304
  }
236
305
 
237
- export { decodeRegionPayload, decodeGridPayload };
306
+ export { decodePayload, decodeGridPayload, decodePayload as decodeRegionPayload };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@truelies/osm-dybuf",
3
- "version": "0.2.6",
3
+ "version": "0.3.0",
4
4
  "description": "Gridex OSM DyBuf parser and schema utilities",
5
5
  "type": "module",
6
6
  "main": "./osm_dybuf.mjs",
package/schema_ids.d.mts CHANGED
@@ -2,6 +2,7 @@ export declare const ENTITY_TYPE_IDS: Readonly<Record<string, number>>;
2
2
  export declare const ID_TO_ENTITY_TYPE: Readonly<Record<number, string>>;
3
3
  export declare const GEOMETRY_KIND_IDS: Readonly<Record<string, number>>;
4
4
  export declare const ID_TO_GEOMETRY_KIND: Readonly<Record<number, string>>;
5
+ export declare const MAIN_FEATURE_IDS: Readonly<Record<string, number>>;
5
6
  export declare const FEATURE_KIND_IDS: Readonly<Record<string, number>>;
6
7
  export declare const ID_TO_FEATURE_KIND: Readonly<Record<number, string>>;
7
8
  export declare const REGION_NUMERIC_CODES: Readonly<Record<string, number>>;
package/schema_ids.mjs CHANGED
@@ -21,20 +21,48 @@ export const GEOMETRY_KIND_IDS = Object.freeze({
21
21
  export const ID_TO_GEOMETRY_KIND = Object.freeze(
22
22
  Object.fromEntries(Object.entries(GEOMETRY_KIND_IDS).map(([k, v]) => [String(v), k]))
23
23
  );
24
- export const FEATURE_KIND_IDS = Object.freeze({
24
+ export const MAIN_FEATURE_IDS = Object.freeze({
25
25
  country_land: 0,
26
- admin_boundary: 100,
27
- coastline: 200,
28
- natural_area: 210,
29
- waterbody: 220,
30
- landuse: 230,
31
- park: 240,
32
- road_major: 300,
33
- road_minor: 301,
34
- waterway: 400,
35
- place_label: 500,
26
+ admin_boundary: 1,
27
+ coastline: 2,
28
+ natural: 3,
29
+ waterbody: 4,
30
+ landuse: 5,
31
+ park: 6,
32
+ road: 7,
33
+ waterway: 8,
34
+ place_label: 9,
36
35
  });
37
36
 
37
+ const FEATURE_KIND_CODES = Object.freeze({
38
+ country_land: ["country_land", 0],
39
+ admin_boundary: ["admin_boundary", 0],
40
+ coastline: ["coastline", 0],
41
+ natural_forest: ["natural", 0],
42
+ natural_vegetation: ["natural", 1],
43
+ natural_bare: ["natural", 2],
44
+ waterbody: ["waterbody", 0],
45
+ landuse: ["landuse", 0],
46
+ park: ["park", 0],
47
+ road_major: ["road", 0],
48
+ road_minor: ["road", 1],
49
+ road_local: ["road", 2],
50
+ road_trail: ["road", 3],
51
+ waterway_major: ["waterway", 0],
52
+ waterway_stream: ["waterway", 1],
53
+ waterway_minor: ["waterway", 2],
54
+ place_label: ["place_label", 0],
55
+ });
56
+
57
+ export const FEATURE_KIND_IDS = Object.freeze(
58
+ Object.fromEntries(
59
+ Object.entries(FEATURE_KIND_CODES).map(([kind, [main, sub]]) => [
60
+ kind,
61
+ sub * 64 + MAIN_FEATURE_IDS[main],
62
+ ])
63
+ )
64
+ );
65
+
38
66
  export const REGION_NUMERIC_CODES = Object.freeze(REGION_CODES_RAW);
39
67
 
40
68
  export const ID_TO_FEATURE_KIND = Object.freeze(