@ferrybig/cgeodata-reader 0.0.0-stage → 0.1.1

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.
Files changed (78) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +313 -2
  3. package/binary-format.md +2620 -0
  4. package/dist/cache.d.ts +24 -0
  5. package/dist/cache.js +60 -0
  6. package/dist/cellBounds.d.ts +19 -0
  7. package/dist/cellBounds.js +36 -0
  8. package/dist/chunk.d.ts +35 -0
  9. package/dist/chunk.js +277 -0
  10. package/dist/chunkDecoder.d.ts +39 -0
  11. package/dist/chunkDecoder.js +116 -0
  12. package/dist/chunkDecoderWasm.d.ts +2 -0
  13. package/dist/chunkDecoderWasm.js +4 -0
  14. package/dist/cli/buildLocateIndex.d.ts +2 -0
  15. package/dist/cli/buildLocateIndex.js +46 -0
  16. package/dist/cli/buildSearchIndex.d.ts +2 -0
  17. package/dist/cli/buildSearchIndex.js +90 -0
  18. package/dist/cli/chunk.d.ts +2 -0
  19. package/dist/cli/chunk.js +67 -0
  20. package/dist/cli/info.d.ts +2 -0
  21. package/dist/cli/info.js +87 -0
  22. package/dist/cli/list.d.ts +2 -0
  23. package/dist/cli/list.js +71 -0
  24. package/dist/cli/locate.d.ts +2 -0
  25. package/dist/cli/locate.js +64 -0
  26. package/dist/cli/lookup.d.ts +2 -0
  27. package/dist/cli/lookup.js +128 -0
  28. package/dist/cli/search.d.ts +2 -0
  29. package/dist/cli/search.js +119 -0
  30. package/dist/cli/shared.d.ts +50 -0
  31. package/dist/cli/shared.js +140 -0
  32. package/dist/cli/show.d.ts +2 -0
  33. package/dist/cli/show.js +38 -0
  34. package/dist/cli.d.ts +2 -0
  35. package/dist/cli.js +107 -0
  36. package/dist/container.d.ts +17 -0
  37. package/dist/container.js +68 -0
  38. package/dist/directory.d.ts +21 -0
  39. package/dist/directory.js +31 -0
  40. package/dist/errors.d.ts +52 -0
  41. package/dist/errors.js +56 -0
  42. package/dist/format.d.ts +22 -0
  43. package/dist/format.js +23 -0
  44. package/dist/header.d.ts +30 -0
  45. package/dist/header.js +69 -0
  46. package/dist/index.d.ts +20 -0
  47. package/dist/index.js +18 -0
  48. package/dist/indexes.d.ts +11 -0
  49. package/dist/indexes.js +12 -0
  50. package/dist/inflate.d.ts +21 -0
  51. package/dist/inflate.js +61 -0
  52. package/dist/locateIndex.d.ts +142 -0
  53. package/dist/locateIndex.js +561 -0
  54. package/dist/morton.d.ts +28 -0
  55. package/dist/morton.js +51 -0
  56. package/dist/node.d.ts +65 -0
  57. package/dist/node.js +137 -0
  58. package/dist/placeTable.d.ts +69 -0
  59. package/dist/placeTable.js +177 -0
  60. package/dist/rangeChunk.d.ts +46 -0
  61. package/dist/rangeChunk.js +401 -0
  62. package/dist/rangeDecoder.d.ts +17 -0
  63. package/dist/rangeDecoder.js +129 -0
  64. package/dist/rangeDecoderWasm.d.ts +2 -0
  65. package/dist/rangeDecoderWasm.js +4 -0
  66. package/dist/reader.d.ts +534 -0
  67. package/dist/reader.js +1132 -0
  68. package/dist/relevance.d.ts +184 -0
  69. package/dist/relevance.js +266 -0
  70. package/dist/searchIndex.d.ts +148 -0
  71. package/dist/searchIndex.js +786 -0
  72. package/dist/source.d.ts +49 -0
  73. package/dist/source.js +50 -0
  74. package/dist/turns.d.ts +19 -0
  75. package/dist/turns.js +40 -0
  76. package/dist/varint.d.ts +52 -0
  77. package/dist/varint.js +137 -0
  78. package/package.json +76 -5
@@ -0,0 +1,561 @@
1
+ /**
2
+ * Place location index (Appendix C of binary-format.md): a sidecar file
3
+ * that stores one representative cell per place, so that
4
+ * `CGeoDataReader.locate()` is a single read of a few bytes instead of a
5
+ * scan through every chunk.
6
+ *
7
+ * The file is CGEODATA kind 2: the 8-byte "CGEODATA" signature and a
8
+ * `version` varint (kind × 16 + version = 33), then an FNV-1a 64-bit
9
+ * checksum over the source's header, directory and place table, the
10
+ * entry width, the place count, one fixed-width entry per place id, and
11
+ * one area byte per place id. Entries are raw, so an entry is found by
12
+ * multiplying its id by the entry width.
13
+ *
14
+ * The representative cell and area of a place are defined in Section
15
+ * C.3, and `locate()` without an index computes exactly the same ones by
16
+ * scanning (`computeLocateCell`). The index is only a precomputed copy of
17
+ * them.
18
+ */
19
+ import { CGeoDataError, CGeoDataFormatError, CGeoDataVersionError, } from "./errors.js";
20
+ import { mortonDecode, mortonEncode, shiftRight } from "./morton.js";
21
+ import { areaByte } from "./relevance.js";
22
+ import { toSource } from "./source.js";
23
+ import { readVarint, writeVarint } from "./varint.js";
24
+ // "CGEODATA" — same signature as the main data file.
25
+ const SIGNATURE = Uint8Array.from([
26
+ 0x43, 0x47, 0x45, 0x4f, 0x44, 0x41, 0x54, 0x41,
27
+ ]);
28
+ const KIND_LOCATE_INDEX = 2;
29
+ // Version 1 added the area bytes (Section C.4); version 0 had only the
30
+ // entries, and a reader that wants areas can't get them from one.
31
+ const FORMAT_VERSION = 1; // kind=2, version=1 → stored varint = 33 (0x21)
32
+ const STORED_VERSION = KIND_LOCATE_INDEX * 16 + FORMAT_VERSION;
33
+ /** Entries are at most 48 bits, so they are exact JavaScript integers
34
+ * (Section C.2). */
35
+ const MAX_ENTRY_WIDTH = 6;
36
+ // ── Entries (Section C.2) ─────────────────────────────────────────────────────
37
+ /** `ceil((width_exp + height_exp + 2 × max_depth + 1) / 8)`: enough bytes
38
+ * for a depth-`max_depth` cell plus its marker bit. */
39
+ export function locateEntryWidth(grid) {
40
+ return Math.ceil((grid.widthExp + grid.heightExp + 2 * grid.maxDepth + 1) / 8);
41
+ }
42
+ /** The entry for `cell`, or `0` ("no geometry") for null. */
43
+ export function encodeLocateEntry(cell, grid) {
44
+ if (!cell)
45
+ return 0;
46
+ const chunkBits = grid.widthExp + grid.heightExp;
47
+ const { depth } = cell;
48
+ const localCode = shiftRight(cell.code, 2 * (grid.maxDepth - depth));
49
+ return (2 ** (chunkBits + 2 * depth) + cell.chunkIndex * 4 ** depth + localCode);
50
+ }
51
+ /** Inverse of `encodeLocateEntry`: null for `0`, and an error for a value
52
+ * whose marker bit doesn't sit at a valid depth. */
53
+ export function decodeLocateEntry(value, grid) {
54
+ if (value === 0)
55
+ return null;
56
+ const chunkBits = grid.widthExp + grid.heightExp;
57
+ let top = 0;
58
+ while (2 ** (top + 1) <= value)
59
+ top++;
60
+ const twiceDepth = top - chunkBits;
61
+ if (twiceDepth < 0 ||
62
+ twiceDepth % 2 !== 0 ||
63
+ twiceDepth > 2 * grid.maxDepth) {
64
+ throw new CGeoDataFormatError(`locate index entry ${value} does not name a cell of this file`);
65
+ }
66
+ const depth = twiceDepth / 2;
67
+ const rest = value - 2 ** top;
68
+ const chunkIndex = Math.floor(rest / 4 ** depth);
69
+ const localCode = rest - chunkIndex * 4 ** depth;
70
+ return { chunkIndex, depth, code: localCode * 4 ** (grid.maxDepth - depth) };
71
+ }
72
+ /** Adds chunk `chunkIndex`'s record `(code, depth)` to `cells` at `pos`. */
73
+ function putRecordCell(cells, pos, chunkIndex, code, depth, header) {
74
+ const D = header.maxDepth;
75
+ const { x, y } = mortonDecode(code, D);
76
+ cells.gx[pos] = (chunkIndex % header.width) * 2 ** D + x;
77
+ cells.gy[pos] = Math.floor(chunkIndex / header.width) * 2 ** D + y;
78
+ cells.depth[pos] = depth;
79
+ }
80
+ /**
81
+ * Throws unless every intermediate value of Section C.3 is an exact
82
+ * JavaScript integer for this grid: entries of at most 48 bits and squared
83
+ * distances below 2^53. Holds for every grid a worldwide build would use.
84
+ */
85
+ function checkGrid(header) {
86
+ const width = locateEntryWidth(header);
87
+ if (width > MAX_ENTRY_WIDTH) {
88
+ throw new CGeoDataError(`a locate index needs ${width}-byte entries for this grid; at most ${MAX_ENTRY_WIDTH} are supported`);
89
+ }
90
+ const { ax, ay } = distanceScale(header);
91
+ const nx = header.width * 2 ** header.maxDepth;
92
+ const ny = header.height * 2 ** header.maxDepth;
93
+ if ((ax * nx) ** 2 + (ay * ny) ** 2 > Number.MAX_SAFE_INTEGER) {
94
+ throw new CGeoDataError("this file's grid is too fine for a locate index");
95
+ }
96
+ }
97
+ /** Per-axis factors that make squared distances proportional to squared
98
+ * distances in degrees: a cell is `360 / NX` wide and `180 / NY` high, a
99
+ * `2H : W` ratio, reduced so that the smaller factor is 1. */
100
+ function distanceScale(header) {
101
+ const g = Math.min(2 * header.height, header.width);
102
+ return { ax: (2 * header.height) / g, ay: header.width / g };
103
+ }
104
+ /**
105
+ * Step 2 of Section C.3 for one axis, in half-cell units: the sum
106
+ * `lo + hi` of the weighted median's two bounds. `keys` holds
107
+ * `centre × 64 + depth` per cell -- centre in half-cells (`2 × g + size`),
108
+ * packed with the depth so a plain numeric sort orders by centre -- and is
109
+ * sorted in place.
110
+ */
111
+ function medianSum(keys, total, maxDepth) {
112
+ keys.sort();
113
+ let cumulative = 0;
114
+ let i = 0;
115
+ while (i < keys.length) {
116
+ const centre = Math.floor(keys[i] / 64);
117
+ for (; i < keys.length && Math.floor(keys[i] / 64) === centre; i++) {
118
+ cumulative += 4 ** (maxDepth - (keys[i] % 64));
119
+ }
120
+ if (2 * cumulative > total)
121
+ return 2 * centre;
122
+ // Exactly half the weight is at or below `centre`, so the rest is
123
+ // above it and the median is midway to the next centre.
124
+ if (2 * cumulative === total)
125
+ return centre + Math.floor(keys[i] / 64);
126
+ }
127
+ throw new Error("unreachable: weights did not add up to their total");
128
+ }
129
+ /**
130
+ * The representative cell of Section C.3 for the cells `start .. end - 1`
131
+ * of `cells`: the full-precision cell nearest to the per-axis weighted
132
+ * median of the cells' centres, among the cells themselves. Returned in
133
+ * global full-precision coordinates; undefined for an empty range.
134
+ */
135
+ export function representativeCell(cells, start, end, header) {
136
+ if (end <= start)
137
+ return undefined;
138
+ const D = header.maxDepth;
139
+ const xKeys = new Float64Array(end - start);
140
+ const yKeys = new Float64Array(end - start);
141
+ let total = 0;
142
+ for (let i = start; i < end; i++) {
143
+ const depth = cells.depth[i];
144
+ const size = 2 ** (D - depth);
145
+ xKeys[i - start] = (2 * cells.gx[i] + size) * 64 + depth;
146
+ yKeys[i - start] = (2 * cells.gy[i] + size) * 64 + depth;
147
+ total += size * size;
148
+ }
149
+ // The median point, as the full-precision cell containing it.
150
+ const fx = Math.floor(medianSum(xKeys, total, D) / 4);
151
+ const fy = Math.floor(medianSum(yKeys, total, D) / 4);
152
+ // Nearest full-precision cell inside one of the cells; the median cell
153
+ // itself when it is inside one (distance 0, and cells don't overlap).
154
+ const { ax, ay } = distanceScale(header);
155
+ let bestDistance = Infinity;
156
+ let bx = 0;
157
+ let by = 0;
158
+ for (let i = start; i < end; i++) {
159
+ const size = 2 ** (D - cells.depth[i]);
160
+ const x0 = cells.gx[i];
161
+ const y0 = cells.gy[i];
162
+ const cx = Math.min(Math.max(fx, x0), x0 + size - 1);
163
+ const cy = Math.min(Math.max(fy, y0), y0 + size - 1);
164
+ const dx = (cx - fx) * ax;
165
+ const dy = (cy - fy) * ay;
166
+ const distance = dx * dx + dy * dy;
167
+ if (distance < bestDistance ||
168
+ (distance === bestDistance && (cy < by || (cy === by && cx < bx)))) {
169
+ bestDistance = distance;
170
+ bx = cx;
171
+ by = cy;
172
+ if (distance === 0)
173
+ break;
174
+ }
175
+ }
176
+ return { gx: bx, gy: by };
177
+ }
178
+ /** A full-precision cell in global coordinates, as the chunk cell an
179
+ * entry names. */
180
+ function toLocateCell(gx, gy, header) {
181
+ const D = header.maxDepth;
182
+ const side = 2 ** D;
183
+ const c = Math.floor(gx / side);
184
+ const r = Math.floor(gy / side);
185
+ return {
186
+ chunkIndex: r * header.width + c,
187
+ code: mortonEncode(gx - c * side, gy - r * side, D),
188
+ depth: D,
189
+ };
190
+ }
191
+ function inRanges(id, ranges) {
192
+ for (const [lo, hi] of ranges)
193
+ if (id >= lo && id < hi)
194
+ return true;
195
+ return false;
196
+ }
197
+ /** The total weight `T` of cells `start .. end - 1` (Section C.3): their
198
+ * area in full-precision cells. */
199
+ function areaCells(cells, start, end, maxDepth) {
200
+ let total = 0;
201
+ for (let i = start; i < end; i++)
202
+ total += 4 ** (maxDepth - cells.depth[i]);
203
+ return total;
204
+ }
205
+ /**
206
+ * Section C.3 for one place, straight from the chunks: collects the cells
207
+ * of every record attributed to `id` or a place under it, then picks the
208
+ * representative cell and totals the area. This is what `locate()` does
209
+ * without an index, and it reads every non-empty chunk.
210
+ */
211
+ export async function computeLocateCell(reader, id) {
212
+ const { header } = reader;
213
+ checkGrid(header);
214
+ const ranges = reader.subtreeIdRanges(id);
215
+ const found = [];
216
+ const chunkCount = reader.directory.chunkLengths.length;
217
+ for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) {
218
+ // Not cached: a one-pass walk over every chunk would only evict
219
+ // what lookups had cached.
220
+ const chunk = await reader.chunk(chunkIndex, { cache: false });
221
+ if (!chunk)
222
+ continue;
223
+ // wanted[k]: whether palette index k is one of the place's.
224
+ const wanted = new Uint8Array(chunk.palette.length + 1);
225
+ let any = false;
226
+ for (let k = 1; k <= chunk.palette.length; k++) {
227
+ if (!inRanges(chunk.palette[k - 1], ranges))
228
+ continue;
229
+ wanted[k] = 1;
230
+ any = true;
231
+ }
232
+ if (!any)
233
+ continue;
234
+ for (let j = 0; j < chunk.codes.length; j++) {
235
+ if (wanted[chunk.paletteIndices[j]]) {
236
+ found.push([chunkIndex, chunk.codes[j], chunk.depths[j]]);
237
+ }
238
+ }
239
+ }
240
+ const cells = {
241
+ gx: new Uint32Array(found.length),
242
+ gy: new Uint32Array(found.length),
243
+ depth: new Uint8Array(found.length),
244
+ };
245
+ found.forEach(([chunkIndex, code, depth], i) => {
246
+ putRecordCell(cells, i, chunkIndex, code, depth, header);
247
+ });
248
+ const best = representativeCell(cells, 0, found.length, header);
249
+ if (!best)
250
+ return null;
251
+ return {
252
+ cell: toLocateCell(best.gx, best.gy, header),
253
+ area: areaByte(areaCells(cells, 0, found.length, header.maxDepth)),
254
+ };
255
+ }
256
+ const DEFAULT_BATCH_RECORDS = 8_000_000;
257
+ /**
258
+ * Builds a locate index (Section C.4) for every place in `reader` and
259
+ * returns the file's bytes.
260
+ *
261
+ * Every place's cells are gathered from the chunks country by country.
262
+ * Within a batch the places are laid out in depth-first order -- a
263
+ * country, then each of its regions followed by that region's cities --
264
+ * so the records of any place and everything under it are one contiguous
265
+ * run, and each place is one call to `representativeCell`.
266
+ */
267
+ export async function buildLocateIndex(reader, options = {}) {
268
+ const { header } = reader;
269
+ checkGrid(header);
270
+ const budget = options.batchRecords ?? DEFAULT_BATCH_RECORDS;
271
+ const { countries: C, regions: R, cities } = reader.placeCounts;
272
+ const N = C + R + cities;
273
+ const chunkCount = reader.directory.chunkLengths.length;
274
+ // Depth-first place order, and where each country's run starts in it.
275
+ const dfsOrder = new Uint32Array(N);
276
+ const countryDfsStart = new Uint32Array(C + 1);
277
+ const countryOf = new Uint32Array(N);
278
+ let n = 0;
279
+ for (let c = 0; c < C; c++) {
280
+ countryDfsStart[c] = n;
281
+ countryOf[c] = c;
282
+ dfsOrder[n++] = c;
283
+ const [lo, hi] = childRange(reader, c);
284
+ for (let region = lo; region < hi; region++) {
285
+ countryOf[region] = c;
286
+ dfsOrder[n++] = region;
287
+ const [cityLo, cityHi] = childRange(reader, region);
288
+ for (let city = cityLo; city < cityHi; city++) {
289
+ countryOf[city] = c;
290
+ dfsOrder[n++] = city;
291
+ }
292
+ }
293
+ }
294
+ countryDfsStart[C] = n;
295
+ // Pass 1: records per place, and which chunks each country appears in.
296
+ const recordCount = new Uint32Array(N);
297
+ const countryChunks = Array.from({ length: C }, () => []);
298
+ for (let chunkIndex = 0; chunkIndex < chunkCount; chunkIndex++) {
299
+ const chunk = await reader.chunk(chunkIndex, { cache: false });
300
+ if (!chunk)
301
+ continue;
302
+ for (const k of chunk.paletteIndices) {
303
+ if (k === 0)
304
+ continue;
305
+ const placeId = chunk.palette[k - 1];
306
+ recordCount[placeId] = recordCount[placeId] + 1;
307
+ }
308
+ for (const placeId of chunk.palette) {
309
+ const list = countryChunks[countryOf[placeId]];
310
+ if (list.at(-1) !== chunkIndex)
311
+ list.push(chunkIndex);
312
+ }
313
+ }
314
+ const entries = new Float64Array(N);
315
+ const areas = new Uint8Array(N);
316
+ const start = new Uint32Array(N); // a place's first slot in its batch
317
+ const cursor = new Uint32Array(N);
318
+ const subtreeEnd = new Uint32Array(N);
319
+ let firstCountry = 0;
320
+ while (firstCountry < C) {
321
+ // Countries firstCountry .. lastCountry - 1 make up this batch.
322
+ let lastCountry = firstCountry;
323
+ let batchSize = 0;
324
+ while (lastCountry < C) {
325
+ let size = 0;
326
+ for (let p = countryDfsStart[lastCountry]; p < countryDfsStart[lastCountry + 1]; p++) {
327
+ size += recordCount[dfsOrder[p]];
328
+ }
329
+ if (lastCountry > firstCountry && batchSize + size > budget)
330
+ break;
331
+ batchSize += size;
332
+ lastCountry++;
333
+ }
334
+ // Slots in depth-first order; subtree ends follow from the layout.
335
+ let slot = 0;
336
+ for (let p = countryDfsStart[firstCountry]; p < countryDfsStart[lastCountry]; p++) {
337
+ const placeId = dfsOrder[p];
338
+ start[placeId] = cursor[placeId] = slot;
339
+ slot += recordCount[placeId];
340
+ }
341
+ for (let c = firstCountry; c < lastCountry; c++) {
342
+ const countryEnd = c + 1 < lastCountry ? start[c + 1] : slot;
343
+ subtreeEnd[c] = countryEnd;
344
+ const [lo, hi] = childRange(reader, c);
345
+ for (let region = lo; region < hi; region++) {
346
+ subtreeEnd[region] = region + 1 < hi ? start[region + 1] : countryEnd;
347
+ const [cityLo, cityHi] = childRange(reader, region);
348
+ for (let city = cityLo; city < cityHi; city++) {
349
+ subtreeEnd[city] = start[city] + recordCount[city];
350
+ }
351
+ }
352
+ }
353
+ // Pass 2: this batch's records, straight into their slots.
354
+ const cells = {
355
+ gx: new Uint32Array(slot),
356
+ gy: new Uint32Array(slot),
357
+ depth: new Uint8Array(slot),
358
+ };
359
+ const batchChunks = new Set();
360
+ for (let c = firstCountry; c < lastCountry; c++) {
361
+ for (const chunkIndex of countryChunks[c])
362
+ batchChunks.add(chunkIndex);
363
+ }
364
+ for (const chunkIndex of [...batchChunks].sort((a, b) => a - b)) {
365
+ const chunk = (await reader.chunk(chunkIndex, { cache: false }));
366
+ const inBatch = chunk.palette.map((placeId) => {
367
+ const c = countryOf[placeId];
368
+ return c >= firstCountry && c < lastCountry;
369
+ });
370
+ for (let j = 0; j < chunk.codes.length; j++) {
371
+ const k = chunk.paletteIndices[j];
372
+ if (k === 0 || !inBatch[k - 1])
373
+ continue;
374
+ const placeId = chunk.palette[k - 1];
375
+ const pos = cursor[placeId];
376
+ cursor[placeId] = pos + 1;
377
+ putRecordCell(cells, pos, chunkIndex, chunk.codes[j], chunk.depths[j], header);
378
+ }
379
+ }
380
+ for (let p = countryDfsStart[firstCountry]; p < countryDfsStart[lastCountry]; p++) {
381
+ const placeId = dfsOrder[p];
382
+ const best = representativeCell(cells, start[placeId], subtreeEnd[placeId], header);
383
+ entries[placeId] = best
384
+ ? encodeLocateEntry(toLocateCell(best.gx, best.gy, header), header)
385
+ : 0;
386
+ areas[placeId] = areaByte(areaCells(cells, start[placeId], subtreeEnd[placeId], header.maxDepth));
387
+ }
388
+ firstCountry = lastCountry;
389
+ }
390
+ return writeLocateIndex(await reader.prefixChecksum(), locateEntryWidth(header), entries, areas);
391
+ }
392
+ /** The ids of a country's regions or a region's cities: the second of
393
+ * `subtreeIdRanges`, which lists the place itself first. */
394
+ function childRange(reader, id) {
395
+ return reader.subtreeIdRanges(id)[1] ?? [0, 0];
396
+ }
397
+ /** Section C.4's layout around already-computed entries and areas. */
398
+ function writeLocateIndex(checksum, entryWidth, entries, areas) {
399
+ const versionBytes = writeVarint(STORED_VERSION);
400
+ const countBytes = writeVarint(entries.length);
401
+ const headerSize = SIGNATURE.length + versionBytes.length + 8 + 1 + countBytes.length;
402
+ const out = new Uint8Array(headerSize + entries.length * entryWidth + areas.length);
403
+ const dv = new DataView(out.buffer);
404
+ let off = 0;
405
+ out.set(SIGNATURE, off);
406
+ off += SIGNATURE.length;
407
+ out.set(versionBytes, off);
408
+ off += versionBytes.length;
409
+ dv.setUint32(off, Number(checksum & 0xffffffffn), true);
410
+ off += 4;
411
+ dv.setUint32(off, Number(checksum >> 32n), true);
412
+ off += 4;
413
+ out[off++] = entryWidth;
414
+ out.set(countBytes, off);
415
+ off += countBytes.length;
416
+ for (let i = 0; i < entries.length; i++) {
417
+ let value = entries[i];
418
+ for (let b = 0; b < entryWidth; b++) {
419
+ out[off++] = value % 256;
420
+ value = Math.floor(value / 256);
421
+ }
422
+ }
423
+ out.set(areas, off);
424
+ return out;
425
+ }
426
+ // ── Reader (Sections C.4, C.5) ────────────────────────────────────────────────
427
+ /** Longest possible header: signature, a version varint, the u64
428
+ * checksum, the entry width byte and a place count varint. */
429
+ const MAX_HEADER_LENGTH = SIGNATURE.length + 8 + 8 + 1 + 8;
430
+ /**
431
+ * An open locate index file (Section C.4). Opening reads only the header,
432
+ * and each `entry()` or `area()` reads just that entry's bytes; only
433
+ * `areas()`, which a search ranked by area needs, keeps anything (one
434
+ * byte per place), so an idle index otherwise costs an open file handle
435
+ * and no memory.
436
+ *
437
+ * Opening does NOT verify the source checksum -- `CGeoDataReader`'s
438
+ * `useLocateIndex()` does that against the file it's attached to.
439
+ */
440
+ export class LocateIndex {
441
+ source;
442
+ /** The index file's name, when its source has one (`ByteSource.name`). */
443
+ name;
444
+ entriesStart;
445
+ areasStart;
446
+ areasPromise;
447
+ /** Checksum of the header, directory and place table it was built
448
+ * from (Section C.4); a reader only accepts the index when it matches
449
+ * its own. */
450
+ sourceChecksum;
451
+ /** Bytes per entry (Section C.2). */
452
+ entryWidth;
453
+ /** How many places it has an entry for. */
454
+ placeCount;
455
+ constructor(source, entriesStart, sourceChecksum, entryWidth, placeCount) {
456
+ this.source = source;
457
+ if (source.name !== undefined)
458
+ this.name = source.name;
459
+ this.entriesStart = entriesStart;
460
+ this.areasStart = entriesStart + placeCount * entryWidth;
461
+ this.sourceChecksum = sourceChecksum;
462
+ this.entryWidth = entryWidth;
463
+ this.placeCount = placeCount;
464
+ }
465
+ /** Opens a locate index file (read on demand, like a `.bin`).
466
+ * Validates signature, kind, version, entry width and length. Takes
467
+ * ownership of a `ByteSource`: `close()` closes it, as does a failed
468
+ * open. */
469
+ static async open(input) {
470
+ const source = toSource(input);
471
+ try {
472
+ return await LocateIndex.parseHeader(source);
473
+ }
474
+ catch (error) {
475
+ await source.close?.();
476
+ throw error;
477
+ }
478
+ }
479
+ static async parseHeader(source) {
480
+ const data = await source.read(0, Math.min(MAX_HEADER_LENGTH, source.size));
481
+ if (data.length < SIGNATURE.length + 1) {
482
+ throw new CGeoDataFormatError("locate index file is too short to be valid");
483
+ }
484
+ for (let i = 0; i < SIGNATURE.length; i++) {
485
+ if (data[i] !== SIGNATURE[i]) {
486
+ throw new CGeoDataFormatError("not a CGEODATA locate index file (wrong magic bytes)");
487
+ }
488
+ }
489
+ let off = SIGNATURE.length;
490
+ const { value: storedVersion, bytesRead: vb } = readVarint(data, off);
491
+ off += vb;
492
+ // Division, not `>>`/`&`: the stored version is a varint and so can
493
+ // reach 2^53, which 32-bit bitwise ops would silently truncate --
494
+ // a crafted 2^32 + 32 would otherwise read back as kind 2, version 0.
495
+ const kind = Math.floor(storedVersion / 16);
496
+ const ver = storedVersion % 16;
497
+ if (kind !== KIND_LOCATE_INDEX) {
498
+ throw new CGeoDataFormatError(`not a locate index file (kind ${kind}, expected ${KIND_LOCATE_INDEX})`);
499
+ }
500
+ if (ver !== FORMAT_VERSION) {
501
+ throw new CGeoDataVersionError(ver === 0
502
+ ? "locate index predates place areas (kind 2, version 0); rebuild it with build-locate-index"
503
+ : `unsupported locate index version ${ver} (expected ${FORMAT_VERSION})`, ver);
504
+ }
505
+ if (data.length < off + 9)
506
+ throw new CGeoDataFormatError("locate index file is too short for its header");
507
+ const dv = new DataView(data.buffer, data.byteOffset, data.byteLength);
508
+ const sourceChecksum = BigInt(dv.getUint32(off, true)) |
509
+ (BigInt(dv.getUint32(off + 4, true)) << 32n);
510
+ off += 8;
511
+ const entryWidth = data[off++];
512
+ if (entryWidth < 1 || entryWidth > MAX_ENTRY_WIDTH) {
513
+ throw new CGeoDataFormatError(`unsupported locate index entry width ${entryWidth} (must be 1 to ${MAX_ENTRY_WIDTH})`);
514
+ }
515
+ const { value: placeCount, bytesRead: pb } = readVarint(data, off);
516
+ off += pb;
517
+ const expectedTotal = off + placeCount * (entryWidth + 1);
518
+ if (source.size !== expectedTotal) {
519
+ throw new CGeoDataFormatError(`locate index has ${source.size} bytes but header implies ${expectedTotal}`);
520
+ }
521
+ return new LocateIndex(source, off, sourceChecksum, entryWidth, placeCount);
522
+ }
523
+ /** Closes the index's source. */
524
+ async close() {
525
+ await this.source.close?.();
526
+ }
527
+ /** Place `id`'s raw entry (Section C.2); `0` means no geometry. */
528
+ async entry(id) {
529
+ this.checkId(id);
530
+ const bytes = await this.source.read(this.entriesStart + id * this.entryWidth, this.entryWidth);
531
+ let value = 0;
532
+ for (let b = this.entryWidth - 1; b >= 0; b--)
533
+ value = value * 256 + bytes[b];
534
+ return value;
535
+ }
536
+ /** Place `id`'s area byte (Section C.3); `0` means no area. */
537
+ async area(id) {
538
+ this.checkId(id);
539
+ const cached = this.areasPromise;
540
+ if (cached)
541
+ return (await cached)[id];
542
+ return (await this.source.read(this.areasStart + id, 1))[0];
543
+ }
544
+ /** Every place's area byte, by id: one read of `placeCount` bytes, the
545
+ * first time, then kept -- what ranking a search by area needs. */
546
+ areas() {
547
+ this.areasPromise ??= this.source
548
+ .read(this.areasStart, this.placeCount)
549
+ .then((bytes) => bytes.slice(), (error) => {
550
+ this.areasPromise = undefined; // try again next time
551
+ throw error;
552
+ });
553
+ return this.areasPromise;
554
+ }
555
+ checkId(id) {
556
+ if (!Number.isInteger(id) || id < 0 || id >= this.placeCount) {
557
+ throw new RangeError(`${id} is not a place id in this locate index (0 <= id < ${this.placeCount})`);
558
+ }
559
+ }
560
+ }
561
+ //# sourceMappingURL=locateIndex.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Morton (Z-order) code helpers, per binary-format.md Section 8.1.
3
+ *
4
+ * Codes can be up to 2 x max_depth bits wide, i.e. up to 52 bits (max_depth
5
+ * <= 26, Section 5). JavaScript's `<<`, `>>`, `&`, `|` operators truncate to
6
+ * 32-bit signed integers, which silently corrupts anything wider -- so every
7
+ * operation on a *code* here uses multiplication/division instead of native
8
+ * bitwise ops. Native bitwise ops are only used on `x`/`y` individually,
9
+ * which are always at most 26 bits and so fit safely in 32 bits.
10
+ */
11
+ /** Right-shift by `bits`, safe for values up to 2^53. */
12
+ export declare function shiftRight(value: number, bits: number): number;
13
+ /** The low `bits` bits of `value`, safe for values up to 2^53. */
14
+ export declare function lowBits(value: number, bits: number): number;
15
+ /**
16
+ * Full-precision Morton code of cell (x, y) at the given depth: interleave
17
+ * the bits of y and x, most significant first, with the y bit placed above
18
+ * the x bit in every pair.
19
+ */
20
+ export declare function mortonEncode(x: number, y: number, depth: number): number;
21
+ /** Inverse of mortonEncode. Not needed by the read-only lookup path (which
22
+ * only ever encodes a query point), but used by the compressor and tests. */
23
+ export declare function mortonDecode(code: number, depth: number): {
24
+ /** Column, 0 to 2^depth - 1, west to east. */
25
+ x: number;
26
+ /** Row, 0 to 2^depth - 1, south to north. */
27
+ y: number;
28
+ };
package/dist/morton.js ADDED
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Morton (Z-order) code helpers, per binary-format.md Section 8.1.
3
+ *
4
+ * Codes can be up to 2 x max_depth bits wide, i.e. up to 52 bits (max_depth
5
+ * <= 26, Section 5). JavaScript's `<<`, `>>`, `&`, `|` operators truncate to
6
+ * 32-bit signed integers, which silently corrupts anything wider -- so every
7
+ * operation on a *code* here uses multiplication/division instead of native
8
+ * bitwise ops. Native bitwise ops are only used on `x`/`y` individually,
9
+ * which are always at most 26 bits and so fit safely in 32 bits.
10
+ */
11
+ /** Right-shift by `bits`, safe for values up to 2^53. */
12
+ export function shiftRight(value, bits) {
13
+ return Math.floor(value / 2 ** bits);
14
+ }
15
+ /** The low `bits` bits of `value`, safe for values up to 2^53. */
16
+ export function lowBits(value, bits) {
17
+ const modulus = 2 ** bits;
18
+ return value - Math.floor(value / modulus) * modulus;
19
+ }
20
+ /**
21
+ * Full-precision Morton code of cell (x, y) at the given depth: interleave
22
+ * the bits of y and x, most significant first, with the y bit placed above
23
+ * the x bit in every pair.
24
+ */
25
+ export function mortonEncode(x, y, depth) {
26
+ let code = 0;
27
+ for (let i = depth - 1; i >= 0; i--) {
28
+ const yBit = (y >>> i) & 1;
29
+ const xBit = (x >>> i) & 1;
30
+ code = code * 2 + yBit;
31
+ code = code * 2 + xBit;
32
+ }
33
+ return code;
34
+ }
35
+ /** Inverse of mortonEncode. Not needed by the read-only lookup path (which
36
+ * only ever encodes a query point), but used by the compressor and tests. */
37
+ export function mortonDecode(code, depth) {
38
+ let x = 0;
39
+ let y = 0;
40
+ let remaining = code;
41
+ for (let i = 0; i < depth; i++) {
42
+ const xBit = lowBits(remaining, 1);
43
+ remaining = shiftRight(remaining, 1);
44
+ const yBit = lowBits(remaining, 1);
45
+ remaining = shiftRight(remaining, 1);
46
+ x |= xBit << i;
47
+ y |= yBit << i;
48
+ }
49
+ return { x, y };
50
+ }
51
+ //# sourceMappingURL=morton.js.map