@ferrybig/cgeodata-reader 0.0.0-stage → 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +313 -2
  3. package/binary-format.md +2366 -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 +34 -0
  9. package/dist/chunk.js +269 -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 +21 -0
  43. package/dist/format.js +22 -0
  44. package/dist/header.d.ts +29 -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/reader.d.ts +534 -0
  61. package/dist/reader.js +1128 -0
  62. package/dist/relevance.d.ts +184 -0
  63. package/dist/relevance.js +266 -0
  64. package/dist/searchIndex.d.ts +148 -0
  65. package/dist/searchIndex.js +786 -0
  66. package/dist/source.d.ts +49 -0
  67. package/dist/source.js +50 -0
  68. package/dist/turns.d.ts +19 -0
  69. package/dist/turns.js +40 -0
  70. package/dist/varint.d.ts +52 -0
  71. package/dist/varint.js +137 -0
  72. package/package.json +76 -5
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A least-recently-used cache bounded by the byte size of what it holds,
3
+ * not by entry count: decoded chunks range from a few hundred bytes to
4
+ * several megabytes (5.4 MB for the largest on a worldwide build), so
5
+ * only a byte budget answers "how much memory does the reader need".
6
+ *
7
+ * A `Map` iterates in insertion order, so re-inserting an entry on every
8
+ * hit keeps the least recently used one first, where eviction takes it.
9
+ */
10
+ export declare class ByteBudgetCache<V> {
11
+ private readonly entries;
12
+ private totalBytes;
13
+ readonly maxBytes: number;
14
+ constructor(maxBytes: number);
15
+ /** Bytes currently held. */
16
+ get size(): number;
17
+ get(key: number): V | undefined;
18
+ /** Caches `value` as `bytes` bytes, evicting least recently used entries
19
+ * to stay within budget. A value bigger than the whole budget isn't
20
+ * cached at all, rather than emptying the cache for it. */
21
+ set(key: number, value: V, bytes: number): void;
22
+ delete(key: number): void;
23
+ clear(): void;
24
+ }
package/dist/cache.js ADDED
@@ -0,0 +1,60 @@
1
+ /**
2
+ * A least-recently-used cache bounded by the byte size of what it holds,
3
+ * not by entry count: decoded chunks range from a few hundred bytes to
4
+ * several megabytes (5.4 MB for the largest on a worldwide build), so
5
+ * only a byte budget answers "how much memory does the reader need".
6
+ *
7
+ * A `Map` iterates in insertion order, so re-inserting an entry on every
8
+ * hit keeps the least recently used one first, where eviction takes it.
9
+ */
10
+ export class ByteBudgetCache {
11
+ entries = new Map();
12
+ totalBytes = 0;
13
+ maxBytes;
14
+ constructor(maxBytes) {
15
+ if (!(maxBytes >= 0)) {
16
+ throw new RangeError(`cache size must be a non-negative number of bytes, not ${maxBytes}`);
17
+ }
18
+ this.maxBytes = maxBytes;
19
+ }
20
+ /** Bytes currently held. */
21
+ get size() {
22
+ return this.totalBytes;
23
+ }
24
+ get(key) {
25
+ const entry = this.entries.get(key);
26
+ if (entry === undefined)
27
+ return undefined;
28
+ this.entries.delete(key);
29
+ this.entries.set(key, entry);
30
+ return entry.value;
31
+ }
32
+ /** Caches `value` as `bytes` bytes, evicting least recently used entries
33
+ * to stay within budget. A value bigger than the whole budget isn't
34
+ * cached at all, rather than emptying the cache for it. */
35
+ set(key, value, bytes) {
36
+ this.delete(key);
37
+ if (bytes > this.maxBytes)
38
+ return;
39
+ this.entries.set(key, { value, bytes });
40
+ this.totalBytes += bytes;
41
+ for (const [oldest, { bytes: oldBytes }] of this.entries) {
42
+ if (this.totalBytes <= this.maxBytes)
43
+ break;
44
+ this.entries.delete(oldest);
45
+ this.totalBytes -= oldBytes;
46
+ }
47
+ }
48
+ delete(key) {
49
+ const entry = this.entries.get(key);
50
+ if (entry === undefined)
51
+ return;
52
+ this.entries.delete(key);
53
+ this.totalBytes -= entry.bytes;
54
+ }
55
+ clear() {
56
+ this.entries.clear();
57
+ this.totalBytes = 0;
58
+ }
59
+ }
60
+ //# sourceMappingURL=cache.js.map
@@ -0,0 +1,19 @@
1
+ /** A lat/lon rectangle, half-open like the rest of the format (Section
2
+ * 2.3): `minLat <= lat < maxLat`, `minLon <= lon < maxLon`. */
3
+ export interface BoundingBox {
4
+ /** Southern edge, included. */
5
+ minLat: number;
6
+ /** Northern edge, excluded. */
7
+ maxLat: number;
8
+ /** Western edge, included. */
9
+ minLon: number;
10
+ /** Eastern edge, excluded. */
11
+ maxLon: number;
12
+ }
13
+ /** The lat/lon extent of chunk (c, r) in a W x H grid (Section 6.1). */
14
+ export declare function chunkBoundingBox(c: number, r: number, width: number, height: number): BoundingBox;
15
+ /** The lat/lon extent of cell (cellX, cellY) at the given depth within a
16
+ * chunk (Section 8.1: cell (0, 0) is the chunk's own south-west corner). */
17
+ export declare function cellBoundingBox(chunkBox: BoundingBox, cellX: number, cellY: number, depth: number): BoundingBox;
18
+ /** Whether `box` contains the point, by the half-open rule above. */
19
+ export declare function boundingBoxContains(box: BoundingBox, lat: number, lon: number): boolean;
@@ -0,0 +1,36 @@
1
+ /** The lat/lon extent of chunk (c, r) in a W x H grid (Section 6.1). */
2
+ export function chunkBoundingBox(c, r, width, height) {
3
+ const lonWidth = 360 / width;
4
+ const latHeight = 180 / height;
5
+ const minLon = -180 + c * lonWidth;
6
+ const minLat = -90 + r * latHeight;
7
+ return {
8
+ minLon,
9
+ maxLon: minLon + lonWidth,
10
+ minLat,
11
+ maxLat: minLat + latHeight,
12
+ };
13
+ }
14
+ /** The lat/lon extent of cell (cellX, cellY) at the given depth within a
15
+ * chunk (Section 8.1: cell (0, 0) is the chunk's own south-west corner). */
16
+ export function cellBoundingBox(chunkBox, cellX, cellY, depth) {
17
+ const cellsPerSide = 2 ** depth;
18
+ const lonWidth = (chunkBox.maxLon - chunkBox.minLon) / cellsPerSide;
19
+ const latHeight = (chunkBox.maxLat - chunkBox.minLat) / cellsPerSide;
20
+ const minLon = chunkBox.minLon + cellX * lonWidth;
21
+ const minLat = chunkBox.minLat + cellY * latHeight;
22
+ return {
23
+ minLon,
24
+ maxLon: minLon + lonWidth,
25
+ minLat,
26
+ maxLat: minLat + latHeight,
27
+ };
28
+ }
29
+ /** Whether `box` contains the point, by the half-open rule above. */
30
+ export function boundingBoxContains(box, lat, lon) {
31
+ return (lat >= box.minLat &&
32
+ lat < box.maxLat &&
33
+ lon >= box.minLon &&
34
+ lon < box.maxLon);
35
+ }
36
+ //# sourceMappingURL=cellBounds.js.map
@@ -0,0 +1,34 @@
1
+ /** One chunk's records (Section 8), decoded: record `j` is the cell
2
+ * `codes[j]` at depth `depths[j]`, attributed to
3
+ * `palette[paletteIndices[j] - 1]` (or to no place for index 0). */
4
+ export interface ChunkRecords {
5
+ /** Global place ids referenced by this chunk, indexed 0..P-1. */
6
+ palette: number[];
7
+ /** Stored Morton codes, chunk-local, strictly increasing. Typed arrays
8
+ * rather than plain ones for this and the other per-record fields: a
9
+ * big chunk holds over a million records, and parsing and holding them
10
+ * as boxed JS arrays costs several times as much. 32 bits, as every
11
+ * code is below 4^`MAX_SUPPORTED_DEPTH`. */
12
+ codes: Uint32Array;
13
+ /** Each record's depth in the chunk's quadtree, 0 to `max_depth`. */
14
+ depths: Uint8Array;
15
+ /** Local palette index per record: 0 = "no place", k = palette[k-1].
16
+ * As wide as the file stores them (Section 8.3's S), rounding 3 bytes
17
+ * up to a `Uint32Array`. */
18
+ paletteIndices: Uint8Array | Uint16Array | Uint32Array;
19
+ }
20
+ /**
21
+ * The deepest `max_depth` this reader supports, though the format allows
22
+ * up to 26: every code is then below 4^16 = 2^32, so a decoded chunk
23
+ * holds its codes in 32 bits. Deeper files aren't worth reading in
24
+ * place: one chunk is one zlib block that a lookup inflates whole, and
25
+ * at depth 16 a chunk already spans up to 4^16 cells, so a lookup could
26
+ * inflate gigabytes. Finer cells should come from a bigger chunk grid
27
+ * (`width_exp`, `height_exp`) instead (binary-format.md Section 10).
28
+ */
29
+ export declare const MAX_SUPPORTED_DEPTH = 16;
30
+ /** Parses one already-inflated chunk block (Section 8.2, or Section 8.6's
31
+ * layout for a `version` 0 file) and checks the cheap Section 8.5
32
+ * invariants a reader can verify without external context (place ids
33
+ * are validated by the caller, which has the place table). */
34
+ export declare function parseChunk(buf: Uint8Array, maxDepth: number, version: number): ChunkRecords;
package/dist/chunk.js ADDED
@@ -0,0 +1,269 @@
1
+ import { decodeChunkCodes, WASM_MIN_RECORDS } from "./chunkDecoder.js";
2
+ import { CGeoDataFormatError } from "./errors.js";
3
+ import { readDeltaVarintList, readDeltaVarints32Into, readVarint, } from "./varint.js";
4
+ /**
5
+ * The deepest `max_depth` this reader supports, though the format allows
6
+ * up to 26: every code is then below 4^16 = 2^32, so a decoded chunk
7
+ * holds its codes in 32 bits. Deeper files aren't worth reading in
8
+ * place: one chunk is one zlib block that a lookup inflates whole, and
9
+ * at depth 16 a chunk already spans up to 4^16 cells, so a lookup could
10
+ * inflate gigabytes. Finer cells should come from a bigger chunk grid
11
+ * (`width_exp`, `height_exp`) instead (binary-format.md Section 10).
12
+ */
13
+ export const MAX_SUPPORTED_DEPTH = 16;
14
+ /** The index width S from Section 8.3's table. */
15
+ function paletteIndexWidth(paletteCount) {
16
+ if (paletteCount <= 255)
17
+ return 1;
18
+ if (paletteCount <= 65535)
19
+ return 2;
20
+ if (paletteCount <= 2 ** 24 - 1)
21
+ return 3;
22
+ return 4;
23
+ }
24
+ /** Whether typed arrays here are little-endian, as the file's integers
25
+ * are (true on every mainstream platform), so 2- and 4-byte palette
26
+ * indices can be copied in rather than assembled byte by byte. */
27
+ const LITTLE_ENDIAN = new Uint8Array(Uint16Array.of(1).buffer)[0] === 1;
28
+ /** `n` unsigned `width`-byte little-endian integers (Section 8.3's
29
+ * palette indices) from `buf` at `offset`, in the narrowest typed array
30
+ * that holds `width` bytes. Always a copy, so the inflated block can be
31
+ * freed. */
32
+ function readLittleEndian(buf, offset, width, n) {
33
+ switch (width) {
34
+ case 1:
35
+ // Not `buf.slice()`: the block is a Buffer, whose `slice` returns
36
+ // a view.
37
+ return new Uint8Array(buf.subarray(offset, offset + n));
38
+ case 2: {
39
+ const out = new Uint16Array(n);
40
+ if (LITTLE_ENDIAN) {
41
+ new Uint8Array(out.buffer).set(buf.subarray(offset, offset + 2 * n));
42
+ return out;
43
+ }
44
+ for (let i = 0, b = offset; i < n; i++, b += 2)
45
+ out[i] = buf[b] | (buf[b + 1] << 8);
46
+ return out;
47
+ }
48
+ case 3: {
49
+ const out = new Uint32Array(n);
50
+ for (let i = 0, b = offset; i < n; i++, b += 3) {
51
+ out[i] = buf[b] | (buf[b + 1] << 8) | (buf[b + 2] << 16);
52
+ }
53
+ return out;
54
+ }
55
+ default: {
56
+ const out = new Uint32Array(n);
57
+ if (LITTLE_ENDIAN) {
58
+ new Uint8Array(out.buffer).set(buf.subarray(offset, offset + 4 * n));
59
+ return out;
60
+ }
61
+ for (let i = 0, b = offset; i < n; i++, b += 4) {
62
+ out[i] =
63
+ buf[b] |
64
+ (buf[b + 1] << 8) |
65
+ (buf[b + 2] << 16) |
66
+ (buf[b + 3] << 24);
67
+ }
68
+ return out;
69
+ }
70
+ }
71
+ }
72
+ /** Whether the power of two `span` divides `value`, both below 2^53: a
73
+ * bitwise check while they fit in 31 bits (max_depth 15 and under), else
74
+ * a double modulo, which is exact but several times slower. */
75
+ function isMultipleOf(value, span) {
76
+ return value < 0x80000000 ? (value & (span - 1)) === 0 : value % span === 0;
77
+ }
78
+ /** Parses one already-inflated chunk block (Section 8.2, or Section 8.6's
79
+ * layout for a `version` 0 file) and checks the cheap Section 8.5
80
+ * invariants a reader can verify without external context (place ids
81
+ * are validated by the caller, which has the place table). */
82
+ export function parseChunk(buf, maxDepth, version) {
83
+ if (maxDepth > MAX_SUPPORTED_DEPTH) {
84
+ throw new RangeError(`max_depth ${maxDepth} is past the ${MAX_SUPPORTED_DEPTH} this reader supports`);
85
+ }
86
+ let offset = 0;
87
+ const { value: paletteCount, bytesRead: b1 } = readVarint(buf, offset);
88
+ offset += b1;
89
+ const { values: palette, bytesRead: b2 } = readDeltaVarintList(buf, offset, paletteCount);
90
+ offset += b2;
91
+ const { value: recordCount, bytesRead: b3 } = readVarint(buf, offset);
92
+ offset += b3;
93
+ if (version !== 0) {
94
+ return parseRecords(buf, offset, palette, recordCount, maxDepth);
95
+ }
96
+ const { value: codesLength, bytesRead: b4 } = readVarint(buf, offset);
97
+ offset += b4;
98
+ // Every code takes at least one byte, so a count the block can't hold
99
+ // fails here instead of allocating for it.
100
+ if (offset + recordCount > buf.length) {
101
+ throw new CGeoDataFormatError("chunk codes run past end of block");
102
+ }
103
+ const codesStart = offset;
104
+ const indexWidth = paletteIndexWidth(paletteCount);
105
+ const depthsStart = codesStart + codesLength;
106
+ if (recordCount >= WASM_MIN_RECORDS &&
107
+ paletteCount < 2 ** 32 &&
108
+ depthsStart + recordCount * (1 + indexWidth) === buf.length) {
109
+ // The fast path (./chunkDecoder.ts) for a block whose size adds up;
110
+ // a declined block falls through to be parsed below.
111
+ const codes = decodeChunkCodes(buf, codesStart, codesLength, recordCount, maxDepth, paletteCount, indexWidth);
112
+ if (codes) {
113
+ return {
114
+ palette,
115
+ codes,
116
+ depths: new Uint8Array(buf.subarray(depthsStart, depthsStart + recordCount)),
117
+ paletteIndices: readLittleEndian(buf, depthsStart + recordCount, indexWidth, recordCount),
118
+ };
119
+ }
120
+ }
121
+ const codes = new Uint32Array(recordCount);
122
+ const codesBytesRead = readDeltaVarints32Into(buf, codesStart, codes) - codesStart;
123
+ if (codesBytesRead !== codesLength) {
124
+ throw new CGeoDataFormatError(`chunk codes_length mismatch: header says ${codesLength}, decoding consumed ${codesBytesRead}`);
125
+ }
126
+ offset = codesStart + codesLength;
127
+ if (offset + recordCount > buf.length) {
128
+ throw new CGeoDataFormatError("chunk depths array runs past end of block");
129
+ }
130
+ // A copy, not a view, so the inflated block can be freed. Not
131
+ // `buf.slice()`: the block is a Buffer, whose `slice` returns a view.
132
+ const depths = new Uint8Array(buf.subarray(offset, offset + recordCount));
133
+ offset += recordCount;
134
+ const indicesBytes = recordCount * indexWidth;
135
+ if (offset + indicesBytes > buf.length) {
136
+ throw new CGeoDataFormatError("chunk palette_indices array runs past end of block");
137
+ }
138
+ const paletteIndices = readLittleEndian(buf, offset, indexWidth, recordCount);
139
+ offset += indicesBytes;
140
+ if (offset !== buf.length) {
141
+ throw new CGeoDataFormatError("chunk block has trailing bytes after palette_indices");
142
+ }
143
+ // span[d]: how many max-depth cells a depth-d record covers, 4^(D-d).
144
+ // A stored code is valid for depth d exactly when its low 2 x (D-d) bits
145
+ // are zero, i.e. when span[d] divides it -- exact in doubles, since codes
146
+ // and spans are both below 2^53.
147
+ const span = [];
148
+ for (let d = 0; d <= maxDepth; d++)
149
+ span.push(4 ** (maxDepth - d));
150
+ const maxCode = 4 ** maxDepth;
151
+ for (let j = 0; j < recordCount; j++) {
152
+ const depth = depths[j];
153
+ const code = codes[j];
154
+ if (depth > maxDepth) {
155
+ throw new CGeoDataFormatError(`record ${j} has depth ${depth} > max_depth ${maxDepth}`);
156
+ }
157
+ if (!isMultipleOf(code, span[depth])) {
158
+ throw new CGeoDataFormatError(`record ${j}'s code is not a valid stored code for depth ${depth}`);
159
+ }
160
+ if (code >= maxCode) {
161
+ throw new CGeoDataFormatError(`record ${j}'s code ${code} is not less than 4^max_depth`);
162
+ }
163
+ if (paletteIndices[j] > paletteCount) {
164
+ throw new CGeoDataFormatError(`record ${j} has palette index ${paletteIndices[j]} > P (${paletteCount})`);
165
+ }
166
+ if (j > 0) {
167
+ const prevCode = codes[j - 1];
168
+ if (!(prevCode < code)) {
169
+ throw new CGeoDataFormatError(`chunk codes are not strictly increasing at record ${j}`);
170
+ }
171
+ if (prevCode + span[depths[j - 1]] > code) {
172
+ throw new CGeoDataFormatError(`record ${j - 1} overlaps record ${j}`);
173
+ }
174
+ }
175
+ }
176
+ return { palette, codes, depths, paletteIndices };
177
+ }
178
+ /** The rest of a chunk block from its `depths` on (Section 8.2): the
179
+ * records tile the chunk from code 0, so each code is where the record
180
+ * before it ends (Section 8.4). */
181
+ function parseRecords(buf, offset, palette, recordCount, maxDepth) {
182
+ const paletteCount = palette.length;
183
+ const indexWidth = paletteIndexWidth(paletteCount);
184
+ const end = offset + recordCount * (1 + indexWidth);
185
+ if (end > buf.length) {
186
+ throw new CGeoDataFormatError("chunk depths and palette_indices run past end of block");
187
+ }
188
+ if (end !== buf.length) {
189
+ throw new CGeoDataFormatError("chunk block has trailing bytes after palette_indices");
190
+ }
191
+ // A copy, not a view, so the inflated block can be freed. Not
192
+ // `buf.slice()`: the block is a Buffer, whose `slice` returns a view.
193
+ const depths = new Uint8Array(buf.subarray(offset, offset + recordCount));
194
+ const paletteIndices = readLittleEndian(buf, offset + recordCount, indexWidth, recordCount);
195
+ const codes = new Uint32Array(recordCount);
196
+ if (!deriveCodes(depths, maxDepth, codes)) {
197
+ throw recordError(depths, maxDepth);
198
+ }
199
+ let maxIndex = 0;
200
+ for (let j = 0; j < recordCount; j++) {
201
+ const k = paletteIndices[j];
202
+ if (k > maxIndex)
203
+ maxIndex = k;
204
+ }
205
+ if (maxIndex > paletteCount) {
206
+ const j = paletteIndices.findIndex((k) => k > paletteCount);
207
+ throw new CGeoDataFormatError(`record ${j} has palette index ${paletteIndices[j]} > P (${paletteCount})`);
208
+ }
209
+ return { palette, codes, depths, paletteIndices };
210
+ }
211
+ /** Fills `codes` with each record's code (Section 8.4), or returns false
212
+ * when a record breaks invariant 1, 2 or 3 of Section 8.5, for
213
+ * `recordError` to say which. In integers while every code and span fits
214
+ * in 31 bits (max_depth 15 and under), else in doubles. */
215
+ function deriveCodes(depths, maxDepth, codes) {
216
+ const n = depths.length;
217
+ // span[d]: how many max-depth cells a depth-d record covers, 4^(D-d);
218
+ // 0 past max_depth.
219
+ if (maxDepth <= 15) {
220
+ const span = new Int32Array(256);
221
+ for (let d = 0; d <= maxDepth; d++)
222
+ span[d] = 1 << (2 * (maxDepth - d));
223
+ const maxCode = 1 << (2 * maxDepth);
224
+ let code = 0;
225
+ for (let j = 0; j < n; j++) {
226
+ const size = span[depths[j]];
227
+ if (size === 0 || (code & (size - 1)) !== 0 || code >= maxCode) {
228
+ return false;
229
+ }
230
+ codes[j] = code;
231
+ code += size;
232
+ }
233
+ return code <= maxCode;
234
+ }
235
+ const span = new Float64Array(256);
236
+ for (let d = 0; d <= maxDepth; d++)
237
+ span[d] = 4 ** (maxDepth - d);
238
+ const maxCode = 4 ** maxDepth;
239
+ let code = 0;
240
+ for (let j = 0; j < n; j++) {
241
+ const size = span[depths[j]];
242
+ if (size === 0 || code % size !== 0 || code >= maxCode)
243
+ return false;
244
+ codes[j] = code;
245
+ code += size;
246
+ }
247
+ return code <= maxCode;
248
+ }
249
+ /** The error for the first record `deriveCodes` refused. */
250
+ function recordError(depths, maxDepth) {
251
+ const maxCode = 4 ** maxDepth;
252
+ let code = 0;
253
+ for (let j = 0; j < depths.length; j++) {
254
+ const depth = depths[j];
255
+ if (depth > maxDepth) {
256
+ return new CGeoDataFormatError(`record ${j} has depth ${depth} > max_depth ${maxDepth}`);
257
+ }
258
+ if (code >= maxCode) {
259
+ return new CGeoDataFormatError(`record ${j} starts past the end of the chunk`);
260
+ }
261
+ const size = 4 ** (maxDepth - depth);
262
+ if (code % size !== 0) {
263
+ return new CGeoDataFormatError(`record ${j} at code ${code} is not aligned to its depth ${depth}`);
264
+ }
265
+ code += size;
266
+ }
267
+ return new CGeoDataFormatError(`record ${depths.length - 1} runs past the end of the chunk`);
268
+ }
269
+ //# sourceMappingURL=chunk.js.map
@@ -0,0 +1,39 @@
1
+ /**
2
+ * `parseChunk`'s fast path for a version 0 file, whose chunks store their
3
+ * codes (binary-format.md Section 8.6): wasm/chunkDecoder.wat, a
4
+ * WebAssembly SIMD decoder for those codes that checks every invariant
5
+ * `parseChunk` does. A version 1 chunk has no codes to decode. It only
6
+ * ever accepts or declines a block: a declined one --
7
+ * malformed, or valid but unusual -- is parsed again in JavaScript,
8
+ * which throws the error or returns the records. Platforms without
9
+ * WebAssembly SIMD (or that forbid compiling it, such as a page whose
10
+ * CSP lacks `'wasm-unsafe-eval'`) always take the JavaScript path.
11
+ *
12
+ * Memory: one instance, its memory grown to the largest block decoded
13
+ * so far (3.3 MB for world.bin's largest chunk), kept between
14
+ * decodes so a burst of them reuses it -- allocating WebAssembly memory
15
+ * costs 25-100 µs -- and dropped once no decode has used it for
16
+ * `IDLE_RELEASE_MS`, so an idle reader holds none.
17
+ */
18
+ /** Below this many records the JavaScript parser is as fast: on
19
+ * world.bin the fast path is level at 16-32 records, 2x faster at 128,
20
+ * and 5-6x faster from 4096 up. */
21
+ export declare const WASM_MIN_RECORDS = 64;
22
+ /** Turns the fast path off (or back on), for tests and benchmarks that
23
+ * compare it against the JavaScript parser. */
24
+ export declare function useWasmChunkDecoder(on: boolean): void;
25
+ /** How many bytes of WebAssembly memory the fast path holds right now:
26
+ * 0 while idle. */
27
+ export declare function wasmChunkDecoderBytes(): number;
28
+ /** Whether this platform runs the fast path. */
29
+ export declare function wasmChunkDecoderAvailable(): boolean;
30
+ /**
31
+ * The codes of the version 0 chunk block `buf` (Section 8.6), whose
32
+ * codes start at `codesStart`, checked against every Section 8.5 invariant `parseChunk`
33
+ * checks -- or undefined when the fast path declined it or isn't
34
+ * available, and the caller should parse the block in JavaScript. The
35
+ * caller has checked that the block is exactly as long as its counts
36
+ * say, that `maxDepth` is at most 16 (`parseChunk` refuses more) and
37
+ * `paletteCount` below 2^32.
38
+ */
39
+ export declare function decodeChunkCodes(buf: Uint8Array, codesStart: number, codesLength: number, recordCount: number, maxDepth: number, paletteCount: number, indexWidth: number): Uint32Array | undefined;
@@ -0,0 +1,116 @@
1
+ import { CHUNK_DECODER_WASM } from "./chunkDecoderWasm.js";
2
+ /**
3
+ * `parseChunk`'s fast path for a version 0 file, whose chunks store their
4
+ * codes (binary-format.md Section 8.6): wasm/chunkDecoder.wat, a
5
+ * WebAssembly SIMD decoder for those codes that checks every invariant
6
+ * `parseChunk` does. A version 1 chunk has no codes to decode. It only
7
+ * ever accepts or declines a block: a declined one --
8
+ * malformed, or valid but unusual -- is parsed again in JavaScript,
9
+ * which throws the error or returns the records. Platforms without
10
+ * WebAssembly SIMD (or that forbid compiling it, such as a page whose
11
+ * CSP lacks `'wasm-unsafe-eval'`) always take the JavaScript path.
12
+ *
13
+ * Memory: one instance, its memory grown to the largest block decoded
14
+ * so far (3.3 MB for world.bin's largest chunk), kept between
15
+ * decodes so a burst of them reuses it -- allocating WebAssembly memory
16
+ * costs 25-100 µs -- and dropped once no decode has used it for
17
+ * `IDLE_RELEASE_MS`, so an idle reader holds none.
18
+ */
19
+ /** Below this many records the JavaScript parser is as fast: on
20
+ * world.bin the fast path is level at 16-32 records, 2x faster at 128,
21
+ * and 5-6x faster from 4096 up. */
22
+ export const WASM_MIN_RECORDS = 64;
23
+ /** How long the instance outlives its last decode. */
24
+ const IDLE_RELEASE_MS = 1000;
25
+ const PAGE_BYTES = 65536;
26
+ /** The compiled module; null once compiling failed. */
27
+ let compiled;
28
+ let instance;
29
+ let releaseTimer;
30
+ let enabled = true;
31
+ /** Turns the fast path off (or back on), for tests and benchmarks that
32
+ * compare it against the JavaScript parser. */
33
+ export function useWasmChunkDecoder(on) {
34
+ enabled = on;
35
+ if (!on)
36
+ release();
37
+ }
38
+ /** How many bytes of WebAssembly memory the fast path holds right now:
39
+ * 0 while idle. */
40
+ export function wasmChunkDecoderBytes() {
41
+ return instance?.memory.buffer.byteLength ?? 0;
42
+ }
43
+ /** Whether this platform runs the fast path. */
44
+ export function wasmChunkDecoderAvailable() {
45
+ return enabled && getInstance() !== undefined;
46
+ }
47
+ function getInstance() {
48
+ if (instance)
49
+ return instance;
50
+ if (compiled === undefined) {
51
+ try {
52
+ // Throws a ReferenceError where there's no WebAssembly at all.
53
+ compiled = new WebAssembly.Module(Uint8Array.from(atob(CHUNK_DECODER_WASM), (c) => c.charCodeAt(0)));
54
+ }
55
+ catch {
56
+ compiled = null;
57
+ }
58
+ }
59
+ if (compiled === null)
60
+ return undefined;
61
+ const exports = new WebAssembly.Instance(compiled).exports;
62
+ instance = {
63
+ memory: exports.memory,
64
+ parse: exports.parse,
65
+ };
66
+ return instance;
67
+ }
68
+ function release() {
69
+ instance = undefined;
70
+ if (releaseTimer !== undefined)
71
+ clearTimeout(releaseTimer);
72
+ releaseTimer = undefined;
73
+ }
74
+ function releaseWhenIdle() {
75
+ if (releaseTimer !== undefined)
76
+ clearTimeout(releaseTimer);
77
+ releaseTimer = setTimeout(release, IDLE_RELEASE_MS);
78
+ // Node.js: don't keep the process alive just to free this.
79
+ releaseTimer.unref?.();
80
+ }
81
+ /**
82
+ * The codes of the version 0 chunk block `buf` (Section 8.6), whose
83
+ * codes start at `codesStart`, checked against every Section 8.5 invariant `parseChunk`
84
+ * checks -- or undefined when the fast path declined it or isn't
85
+ * available, and the caller should parse the block in JavaScript. The
86
+ * caller has checked that the block is exactly as long as its counts
87
+ * say, that `maxDepth` is at most 16 (`parseChunk` refuses more) and
88
+ * `paletteCount` below 2^32.
89
+ */
90
+ export function decodeChunkCodes(buf, codesStart, codesLength, recordCount, maxDepth, paletteCount, indexWidth) {
91
+ if (!enabled)
92
+ return undefined;
93
+ const decoder = getInstance();
94
+ if (!decoder)
95
+ return undefined;
96
+ const blockBytes = buf.length - codesStart;
97
+ // 16 bytes of slack past the block: a 3-byte index is read as 4.
98
+ const out = Math.ceil((blockBytes + 16) / 16) * 16;
99
+ const needed = out + 4 * recordCount;
100
+ const { memory } = decoder;
101
+ if (memory.buffer.byteLength < needed) {
102
+ try {
103
+ memory.grow(Math.ceil((needed - memory.buffer.byteLength) / PAGE_BYTES));
104
+ }
105
+ catch {
106
+ return undefined; // out of memory: leave it to the JavaScript path
107
+ }
108
+ }
109
+ new Uint8Array(memory.buffer, 0, blockBytes).set(buf.subarray(codesStart));
110
+ const ok = decoder.parse(codesLength, recordCount, maxDepth, paletteCount, indexWidth, out);
111
+ releaseWhenIdle();
112
+ if (!ok)
113
+ return undefined;
114
+ return new Uint32Array(memory.buffer, out, recordCount).slice();
115
+ }
116
+ //# sourceMappingURL=chunkDecoder.js.map
@@ -0,0 +1,2 @@
1
+ /** chunkDecoder.wat's compiled module (1553 bytes), base64. */
2
+ export declare const CHUNK_DECODER_WASM = "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";
@@ -0,0 +1,4 @@
1
+ // Generated by `npm run build:wasm -w reader` from wasm/chunkDecoder.wat; do not edit.
2
+ /** chunkDecoder.wat's compiled module (1553 bytes), base64. */
3
+ export const CHUNK_DECODER_WASM = "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";
4
+ //# sourceMappingURL=chunkDecoderWasm.js.map
@@ -0,0 +1,2 @@
1
+ import { type Command } from "./shared.ts";
2
+ export declare const buildLocateIndexCmd: Command;
@@ -0,0 +1,46 @@
1
+ import { writeFileSync } from "node:fs";
2
+ import { buildLocateIndex } from "../locateIndex.js";
3
+ import { EXIT_OK, openReader, parse, UsageError, } from "./shared.js";
4
+ export const buildLocateIndexCmd = {
5
+ name: "build-locate-index",
6
+ summary: "build a place location index for a .bin file",
7
+ usage: `usage: cgeodata build-locate-index <file.bin> [--output <index.bin>]
8
+
9
+ Computes a representative cell for every place in <file.bin> and writes
10
+ the result as a locate index to <index.bin> (or stdout when --output is
11
+ omitted). Written next to <file.bin> as <file>.locate-index.bin,
12
+ \`cgeodata locate\` and \`search --locate\` (and any CGeoDataReader opened
13
+ from that path) use it automatically; elsewhere, pass it with
14
+ \`locate --index\`. Turns a locate from a scan of every chunk into a
15
+ read of one entry.
16
+
17
+ --output F write to F instead of stdout`,
18
+ async run(argv) {
19
+ const { values, positionals } = parse(argv, {
20
+ output: { type: "string" },
21
+ });
22
+ const [file, ...extra] = positionals;
23
+ if (extra.length > 0)
24
+ throw new UsageError(`unexpected argument ${JSON.stringify(extra[0])}`);
25
+ const reader = await openReader(file);
26
+ const { countries, regions, cities } = reader.placeCounts;
27
+ process.stderr.write(`building locate index over ${countries + regions + cities} places...\n`);
28
+ const t0 = performance.now();
29
+ let bytes;
30
+ try {
31
+ bytes = await buildLocateIndex(reader);
32
+ }
33
+ finally {
34
+ await reader.close();
35
+ }
36
+ process.stderr.write(`done in ${((performance.now() - t0) / 1000).toFixed(1)} s, ${(bytes.length / 1024 / 1024).toFixed(1)} MiB\n`);
37
+ if (values.output !== undefined) {
38
+ writeFileSync(values.output, bytes);
39
+ }
40
+ else {
41
+ process.stdout.write(bytes);
42
+ }
43
+ return EXIT_OK;
44
+ },
45
+ };
46
+ //# sourceMappingURL=buildLocateIndex.js.map