@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
package/dist/node.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ import type { Inflater } from "./inflate.ts";
2
+ import { CGeoDataReader, type OpenOptions } from "./reader.ts";
3
+ import type { ByteSource } from "./source.ts";
4
+ /**
5
+ * A file on disk as a `ByteSource`: each read reads just that range, on
6
+ * libuv's thread pool.
7
+ *
8
+ * Holds a plain file descriptor rather than a `fs.promises` `FileHandle`:
9
+ * Node.js treats a `FileHandle` that is garbage collected without being
10
+ * closed as an uncaught error, which would take a whole server down over
11
+ * a forgotten `close()`. A forgotten `FileSource` is closed when it's
12
+ * collected instead.
13
+ */
14
+ export declare class FileSource implements ByteSource {
15
+ private fd;
16
+ readonly size: number;
17
+ readonly name: string;
18
+ private constructor();
19
+ /** Opens the file at `path` for reading. */
20
+ static open(path: string): Promise<FileSource>;
21
+ read(offset: number, length: number): Promise<Uint8Array>;
22
+ close(): Promise<void>;
23
+ }
24
+ /**
25
+ * Node's zlib, on the calling thread. Deliberately not zlib's async API:
26
+ * measured on a worldwide file, handing blocks to libuv's thread pool made
27
+ * a burst of zoomed-out map tiles 15-40% slower and cold lookups ~30%
28
+ * slower, while barely shortening the longest event-loop stall -- parsing
29
+ * a decoded chunk costs more than inflating it, and that stays on this
30
+ * thread either way. What keeps the event loop responsive is that every
31
+ * chunk read (`FileSource.read`) is asynchronous, so the loop gets a turn
32
+ * between chunks: a stall is one chunk's decode (~25 ms for the largest
33
+ * chunk of a worldwide file, most far less), not a whole tile's or batch's.
34
+ */
35
+ export declare const nodeInflater: Inflater;
36
+ /** Where a search index for `binPath` conventionally lives, and where
37
+ * `openFile()` looks for one: `data/03-final/world.bin` →
38
+ * `data/03-final/world.search-index.bin`. */
39
+ export declare function searchIndexPathFor(binPath: string): string;
40
+ /** Where a locate index for `binPath` conventionally lives, and where
41
+ * `openFile()` looks for one: `data/03-final/world.bin` →
42
+ * `data/03-final/world.locate-index.bin`. */
43
+ export declare function locateIndexPathFor(binPath: string): string;
44
+ /**
45
+ * Opens a `.bin` file from disk, reading only what each lookup needs, with
46
+ * Node's zlib as its inflater.
47
+ *
48
+ * The search and locate indexes next to it (`searchIndexPathFor`,
49
+ * `locateIndexPathFor`) are used automatically when they exist: each is
50
+ * opened on the first `search()` / `locate()` that can use it. One that
51
+ * exists but doesn't belong to this file (built from an older build of
52
+ * it, say) makes those calls reject with a `CGeoDataIndexMismatchError`
53
+ * naming it, rather than silently falling back to a slow scan; rebuild or
54
+ * delete it. `options.searchIndex` / `options.locateIndex` override the
55
+ * lookup next to the file.
56
+ *
57
+ * @example
58
+ * ```ts
59
+ * const reader = await openFile("data/world.bin");
60
+ * const { city } = await reader.query(52.37, 4.9);
61
+ * city?.internationalName; // "Amsterdam"
62
+ * await reader.close();
63
+ * ```
64
+ */
65
+ export declare function openFile(path: string, options?: OpenOptions): Promise<CGeoDataReader>;
package/dist/node.js ADDED
@@ -0,0 +1,137 @@
1
+ /**
2
+ * The Node.js side of the reader: files on disk, and zlib. Everything
3
+ * else (`CGeoDataReader` and friends) is platform-neutral and lives in the
4
+ * package's main entry.
5
+ *
6
+ * @module @ferrybig/cgeodata-reader/node
7
+ */
8
+ import { close, fstat, open, read } from "node:fs";
9
+ import { promisify } from "node:util";
10
+ import { inflateSync } from "node:zlib";
11
+ import { CGeoDataError } from "./errors.js";
12
+ import { CGeoDataReader } from "./reader.js";
13
+ const openAsync = promisify(open);
14
+ const fstatAsync = promisify(fstat);
15
+ const closeAsync = promisify(close);
16
+ /** Closes the descriptor of a `FileSource` that was garbage collected
17
+ * without `close()`, instead of leaking it. */
18
+ const unclosed = new FinalizationRegistry((fd) => close(fd, () => { }));
19
+ /**
20
+ * A file on disk as a `ByteSource`: each read reads just that range, on
21
+ * libuv's thread pool.
22
+ *
23
+ * Holds a plain file descriptor rather than a `fs.promises` `FileHandle`:
24
+ * Node.js treats a `FileHandle` that is garbage collected without being
25
+ * closed as an uncaught error, which would take a whole server down over
26
+ * a forgotten `close()`. A forgotten `FileSource` is closed when it's
27
+ * collected instead.
28
+ */
29
+ export class FileSource {
30
+ fd;
31
+ size;
32
+ name;
33
+ constructor(fd, size, path) {
34
+ this.fd = fd;
35
+ this.size = size;
36
+ this.name = path;
37
+ unclosed.register(this, fd, this);
38
+ }
39
+ /** Opens the file at `path` for reading. */
40
+ static async open(path) {
41
+ const fd = await openAsync(path, "r");
42
+ try {
43
+ return new FileSource(fd, (await fstatAsync(fd)).size, path);
44
+ }
45
+ catch (error) {
46
+ await closeAsync(fd);
47
+ throw error;
48
+ }
49
+ }
50
+ async read(offset, length) {
51
+ const wanted = Math.max(0, Math.min(length, this.size - offset));
52
+ const buffer = new Uint8Array(wanted);
53
+ if (wanted > 0) {
54
+ // The byte count has to be checked: a short read (the file being
55
+ // truncated after it was opened) would otherwise leave the tail of
56
+ // `buffer` zeroed, and zeros parse as perfectly good varints -- a
57
+ // silently wrong answer instead of an error.
58
+ const bytesRead = await new Promise((resolve, reject) => read(this.fd, buffer, 0, wanted, offset, (error, bytesRead) => error ? reject(error) : resolve(bytesRead)));
59
+ if (bytesRead !== wanted) {
60
+ throw new CGeoDataError(`short read: wanted ${wanted} bytes at offset ${offset} of ${this.name}, got ${bytesRead}`);
61
+ }
62
+ }
63
+ return buffer;
64
+ }
65
+ async close() {
66
+ if (this.fd < 0)
67
+ return;
68
+ const fd = this.fd;
69
+ this.fd = -1;
70
+ unclosed.unregister(this);
71
+ await closeAsync(fd);
72
+ }
73
+ }
74
+ /**
75
+ * Node's zlib, on the calling thread. Deliberately not zlib's async API:
76
+ * measured on a worldwide file, handing blocks to libuv's thread pool made
77
+ * a burst of zoomed-out map tiles 15-40% slower and cold lookups ~30%
78
+ * slower, while barely shortening the longest event-loop stall -- parsing
79
+ * a decoded chunk costs more than inflating it, and that stays on this
80
+ * thread either way. What keeps the event loop responsive is that every
81
+ * chunk read (`FileSource.read`) is asynchronous, so the loop gets a turn
82
+ * between chunks: a stall is one chunk's decode (~25 ms for the largest
83
+ * chunk of a worldwide file, most far less), not a whole tile's or batch's.
84
+ */
85
+ export const nodeInflater = (compressed, maxOutputLength) => inflateSync(compressed, { maxOutputLength });
86
+ /** Where a search index for `binPath` conventionally lives, and where
87
+ * `openFile()` looks for one: `data/03-final/world.bin` →
88
+ * `data/03-final/world.search-index.bin`. */
89
+ export function searchIndexPathFor(binPath) {
90
+ return `${binPath.replace(/\.bin$/, "")}.search-index.bin`;
91
+ }
92
+ /** Where a locate index for `binPath` conventionally lives, and where
93
+ * `openFile()` looks for one: `data/03-final/world.bin` →
94
+ * `data/03-final/world.locate-index.bin`. */
95
+ export function locateIndexPathFor(binPath) {
96
+ return `${binPath.replace(/\.bin$/, "")}.locate-index.bin`;
97
+ }
98
+ /**
99
+ * Opens a `.bin` file from disk, reading only what each lookup needs, with
100
+ * Node's zlib as its inflater.
101
+ *
102
+ * The search and locate indexes next to it (`searchIndexPathFor`,
103
+ * `locateIndexPathFor`) are used automatically when they exist: each is
104
+ * opened on the first `search()` / `locate()` that can use it. One that
105
+ * exists but doesn't belong to this file (built from an older build of
106
+ * it, say) makes those calls reject with a `CGeoDataIndexMismatchError`
107
+ * naming it, rather than silently falling back to a slow scan; rebuild or
108
+ * delete it. `options.searchIndex` / `options.locateIndex` override the
109
+ * lookup next to the file.
110
+ *
111
+ * @example
112
+ * ```ts
113
+ * const reader = await openFile("data/world.bin");
114
+ * const { city } = await reader.query(52.37, 4.9);
115
+ * city?.internationalName; // "Amsterdam"
116
+ * await reader.close();
117
+ * ```
118
+ */
119
+ export async function openFile(path, options = {}) {
120
+ return CGeoDataReader.open(await FileSource.open(path), {
121
+ inflater: nodeInflater,
122
+ searchIndex: () => openIfExists(searchIndexPathFor(path)),
123
+ locateIndex: () => openIfExists(locateIndexPathFor(path)),
124
+ ...options,
125
+ });
126
+ }
127
+ async function openIfExists(path) {
128
+ try {
129
+ return await FileSource.open(path);
130
+ }
131
+ catch (error) {
132
+ if (error.code === "ENOENT")
133
+ return undefined;
134
+ throw error;
135
+ }
136
+ }
137
+ //# sourceMappingURL=node.js.map
@@ -0,0 +1,69 @@
1
+ import { type Inflater } from "./inflate.ts";
2
+ import type { ByteSource } from "./source.ts";
3
+ /** The place table's uncompressed part (Section 7.2), parsed: the place
4
+ * counts and where each name block is. */
5
+ export interface PlaceTableInfo {
6
+ /** C, the number of countries. */
7
+ countryCount: number;
8
+ /** R, the number of regions. */
9
+ totalRegions: number;
10
+ /** The number of cities. */
11
+ totalCities: number;
12
+ /** N, every place: countries, regions and cities. */
13
+ totalPlaces: number;
14
+ /** Length countryCount + 1; prefix[c] = number of regions before country c. */
15
+ regionCountryPrefix: number[];
16
+ /** Length totalRegions + 1; prefix[r] = number of cities before region r. */
17
+ cityRegionPrefix: number[];
18
+ /** The header's keyframe_exp (Section 7.3). */
19
+ keyframeExp: number;
20
+ /** Absolute file offset of each name block's compressed bytes. */
21
+ blockOffsets: number[];
22
+ /** Compressed byte length of each name block. */
23
+ blockLengths: number[];
24
+ }
25
+ /** Parses Section 7.2's uncompressed prefix (counts + block_lengths),
26
+ * growing the read like readHeaderAndDirectory does, since its size depends
27
+ * on country_count/region_counts which aren't known up front. */
28
+ export declare function parsePlaceTable(source: ByteSource, placeTableStart: number, placeTableLength: number, keyframeExp: number): Promise<PlaceTableInfo>;
29
+ /** Largest index c such that prefix[c] <= value, per Section 7.5. `prefix`
30
+ * is non-decreasing with prefix[0] = 0. */
31
+ export declare function largestIndexWithPrefixAtMost(prefix: number[], value: number): number;
32
+ /** A place id classified (Section 7.5): its level, and the level-local
33
+ * index of it and of its ancestors -- a region index, not a global id. */
34
+ export type ResolvedPlace = {
35
+ /** A country. */
36
+ level: "country";
37
+ /** The country's index, which is also its global id. */
38
+ countryId: number;
39
+ } | {
40
+ /** A region. */
41
+ level: "region";
42
+ /** Its country's index. */
43
+ countryId: number;
44
+ /** The region's index among all regions. */
45
+ regionId: number;
46
+ } | {
47
+ /** A city. */
48
+ level: "city";
49
+ /** Its country's index. */
50
+ countryId: number;
51
+ /** Its region's index among all regions. */
52
+ regionId: number;
53
+ /** The city's index among all cities. */
54
+ cityId: number;
55
+ };
56
+ /** Classifies a place id per Section 7.5. `regionId`/`cityId` are the
57
+ * level-local indices the spec calls "region index"/"city index" (not
58
+ * global ids) -- add countryCount / countryCount+totalRegions to get the
59
+ * global id for a name lookup. */
60
+ export declare function resolveIdentifier(info: PlaceTableInfo, id: number): ResolvedPlace;
61
+ /** Inflates one whole name block (Section 7.3) and splits it into its
62
+ * entries, each split on `|` (Section 7.4). For walking many consecutive
63
+ * ids this is one inflate per block instead of one per id. */
64
+ export declare function readNameBlock(source: ByteSource, info: PlaceTableInfo, blockIndex: number, inflater: Inflater,
65
+ /** Where the CPU-bound part runs; see `TurnQueue`. */
66
+ decode?: <T>(work: () => Promise<T>) => Promise<T>): Promise<string[][]>;
67
+ /** Recovers a place's name entry (Section 7.4, split on `|`) by inflating
68
+ * only the one name block (Section 7.3) that contains it. */
69
+ export declare function readNameEntry(source: ByteSource, info: PlaceTableInfo, globalId: number, inflater: Inflater): Promise<string[]>;
@@ -0,0 +1,177 @@
1
+ import { CGeoDataFormatError } from "./errors.js";
2
+ import { inflateBlock } from "./inflate.js";
3
+ import { IncompleteBufferError, readVarint, readVarintList } from "./varint.js";
4
+ const utf8 = new TextDecoder();
5
+ function sum(values) {
6
+ return values.reduce((a, b) => a + b, 0);
7
+ }
8
+ function prefixSum(values) {
9
+ const prefix = new Array(values.length + 1);
10
+ prefix[0] = 0;
11
+ for (let i = 0; i < values.length; i++) {
12
+ prefix[i + 1] = prefix[i] + values[i];
13
+ }
14
+ return prefix;
15
+ }
16
+ /** Parses Section 7.2's uncompressed prefix (counts + block_lengths),
17
+ * growing the read like readHeaderAndDirectory does, since its size depends
18
+ * on country_count/region_counts which aren't known up front. */
19
+ export async function parsePlaceTable(source, placeTableStart, placeTableLength, keyframeExp) {
20
+ let size = Math.min(1024, placeTableLength);
21
+ for (;;) {
22
+ const buf = await source.read(placeTableStart, size);
23
+ try {
24
+ return parsePrefix(buf, placeTableStart, placeTableLength, keyframeExp);
25
+ }
26
+ catch (err) {
27
+ if (err instanceof IncompleteBufferError && size < placeTableLength) {
28
+ size = Math.min(size * 4, placeTableLength);
29
+ continue;
30
+ }
31
+ throw err;
32
+ }
33
+ }
34
+ }
35
+ function parsePrefix(buf, placeTableStart, placeTableLength, keyframeExp) {
36
+ let offset = 0;
37
+ const { value: countryCount, bytesRead: b1 } = readVarint(buf, offset);
38
+ offset += b1;
39
+ const { values: regionCounts, bytesRead: b2 } = readVarintList(buf, offset, countryCount);
40
+ offset += b2;
41
+ const totalRegions = sum(regionCounts);
42
+ const { values: cityCounts, bytesRead: b3 } = readVarintList(buf, offset, totalRegions);
43
+ offset += b3;
44
+ const totalCities = sum(cityCounts);
45
+ const totalPlaces = countryCount + totalRegions + totalCities;
46
+ // Section 7.3.
47
+ const blockSize = keyframeExp === 0 ? totalPlaces : 2 ** keyframeExp;
48
+ const blockCount = totalPlaces === 0
49
+ ? 0
50
+ : keyframeExp === 0
51
+ ? 1
52
+ : Math.ceil(totalPlaces / blockSize);
53
+ const storedBlockLengthCount = Math.max(blockCount - 1, 0);
54
+ const { values: storedBlockLengths, bytesRead: b4 } = readVarintList(buf, offset, storedBlockLengthCount);
55
+ offset += b4;
56
+ const namesStart = placeTableStart + offset;
57
+ const blockOffsets = new Array(blockCount);
58
+ const blockLengths = new Array(blockCount);
59
+ let running = namesStart;
60
+ for (let b = 0; b < blockCount - 1; b++) {
61
+ blockOffsets[b] = running;
62
+ blockLengths[b] = storedBlockLengths[b];
63
+ running += storedBlockLengths[b];
64
+ }
65
+ if (blockCount > 0) {
66
+ // The last block's length isn't stored -- it extends to the end of the
67
+ // place table (Section 7.3).
68
+ blockOffsets[blockCount - 1] = running;
69
+ blockLengths[blockCount - 1] = placeTableStart + placeTableLength - running;
70
+ }
71
+ return {
72
+ countryCount,
73
+ totalRegions,
74
+ totalCities,
75
+ totalPlaces,
76
+ regionCountryPrefix: prefixSum(regionCounts),
77
+ cityRegionPrefix: prefixSum(cityCounts),
78
+ keyframeExp,
79
+ blockOffsets,
80
+ blockLengths,
81
+ };
82
+ }
83
+ /** Largest index c such that prefix[c] <= value, per Section 7.5. `prefix`
84
+ * is non-decreasing with prefix[0] = 0. */
85
+ export function largestIndexWithPrefixAtMost(prefix, value) {
86
+ let lo = 0;
87
+ let hi = prefix.length - 2;
88
+ let answer = 0;
89
+ while (lo <= hi) {
90
+ const mid = (lo + hi) >>> 1;
91
+ if (prefix[mid] <= value) {
92
+ answer = mid;
93
+ lo = mid + 1;
94
+ }
95
+ else {
96
+ hi = mid - 1;
97
+ }
98
+ }
99
+ return answer;
100
+ }
101
+ /** Classifies a place id per Section 7.5. `regionId`/`cityId` are the
102
+ * level-local indices the spec calls "region index"/"city index" (not
103
+ * global ids) -- add countryCount / countryCount+totalRegions to get the
104
+ * global id for a name lookup. */
105
+ export function resolveIdentifier(info, id) {
106
+ const { countryCount, totalRegions, totalPlaces, regionCountryPrefix, cityRegionPrefix, } = info;
107
+ if (!Number.isInteger(id) || id < 0 || id >= totalPlaces) {
108
+ throw new RangeError(`place id ${id} is out of range for this file (0 <= id < ${totalPlaces})`);
109
+ }
110
+ if (id < countryCount) {
111
+ return { level: "country", countryId: id };
112
+ }
113
+ if (id < countryCount + totalRegions) {
114
+ const regionId = id - countryCount;
115
+ const countryId = largestIndexWithPrefixAtMost(regionCountryPrefix, regionId);
116
+ return { level: "region", countryId, regionId };
117
+ }
118
+ const cityId = id - countryCount - totalRegions;
119
+ const regionId = largestIndexWithPrefixAtMost(cityRegionPrefix, cityId);
120
+ const countryId = largestIndexWithPrefixAtMost(regionCountryPrefix, regionId);
121
+ return { level: "city", countryId, regionId, cityId };
122
+ }
123
+ /** Inflates one whole name block (Section 7.3) and splits it into its
124
+ * entries, each split on `|` (Section 7.4). For walking many consecutive
125
+ * ids this is one inflate per block instead of one per id. */
126
+ export async function readNameBlock(source, info, blockIndex, inflater,
127
+ /** Where the CPU-bound part runs; see `TurnQueue`. */
128
+ decode = (work) => work()) {
129
+ const { blockOffsets, blockLengths } = info;
130
+ if (blockIndex < 0 || blockIndex >= blockOffsets.length) {
131
+ throw new RangeError(`name block ${blockIndex} is out of range for this file (0 <= block < ${blockOffsets.length})`);
132
+ }
133
+ const compressed = await source.read(blockOffsets[blockIndex], blockLengths[blockIndex]);
134
+ return decode(async () => splitNameBlock(await inflateBlock(inflater, compressed), blockIndex));
135
+ }
136
+ /** An inflated name block (Section 7.3) split into its entries, each
137
+ * split on `|` (Section 7.4). */
138
+ function splitNameBlock(inflated, blockIndex) {
139
+ const end = inflated.lastIndexOf(0) + 1;
140
+ if (end !== inflated.length) {
141
+ throw new CGeoDataFormatError(`name block ${blockIndex} has ${inflated.length - end} trailing bytes after its last NUL terminator`);
142
+ }
143
+ if (end === 0)
144
+ return [];
145
+ // One decode for the whole block: NUL and `|` are single bytes that
146
+ // never occur inside a multi-byte UTF-8 sequence, so splitting the
147
+ // decoded text finds the same boundaries splitting the bytes would.
148
+ const entries = utf8.decode(inflated.subarray(0, end - 1)).split("\0");
149
+ return entries.map((entry) => entry.split("|"));
150
+ }
151
+ /** Recovers a place's name entry (Section 7.4, split on `|`) by inflating
152
+ * only the one name block (Section 7.3) that contains it. */
153
+ export async function readNameEntry(source, info, globalId, inflater) {
154
+ const { keyframeExp, totalPlaces, blockOffsets, blockLengths } = info;
155
+ if (globalId < 0 || globalId >= totalPlaces) {
156
+ throw new RangeError(`place id ${globalId} is out of range for this file (0 <= id < ${totalPlaces})`);
157
+ }
158
+ const blockSize = keyframeExp === 0 ? totalPlaces : 2 ** keyframeExp;
159
+ const blockIndex = Math.floor(globalId / blockSize);
160
+ const withinBlock = globalId - blockIndex * blockSize;
161
+ const compressed = await source.read(blockOffsets[blockIndex], blockLengths[blockIndex]);
162
+ const inflated = await inflateBlock(inflater, compressed);
163
+ let start = 0;
164
+ for (let i = 0; i < withinBlock; i++) {
165
+ const nul = inflated.indexOf(0, start);
166
+ if (nul === -1) {
167
+ throw new CGeoDataFormatError(`name block ${blockIndex} ended before reaching entry ${withinBlock}`);
168
+ }
169
+ start = nul + 1;
170
+ }
171
+ const end = inflated.indexOf(0, start);
172
+ if (end === -1) {
173
+ throw new CGeoDataFormatError(`name entry for place ${globalId} is missing its NUL terminator`);
174
+ }
175
+ return utf8.decode(inflated.subarray(start, end)).split("|");
176
+ }
177
+ //# sourceMappingURL=placeTable.js.map