@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.
- package/LICENSE +21 -0
- package/README.md +313 -2
- package/binary-format.md +2366 -0
- package/dist/cache.d.ts +24 -0
- package/dist/cache.js +60 -0
- package/dist/cellBounds.d.ts +19 -0
- package/dist/cellBounds.js +36 -0
- package/dist/chunk.d.ts +34 -0
- package/dist/chunk.js +269 -0
- package/dist/chunkDecoder.d.ts +39 -0
- package/dist/chunkDecoder.js +116 -0
- package/dist/chunkDecoderWasm.d.ts +2 -0
- package/dist/chunkDecoderWasm.js +4 -0
- package/dist/cli/buildLocateIndex.d.ts +2 -0
- package/dist/cli/buildLocateIndex.js +46 -0
- package/dist/cli/buildSearchIndex.d.ts +2 -0
- package/dist/cli/buildSearchIndex.js +90 -0
- package/dist/cli/chunk.d.ts +2 -0
- package/dist/cli/chunk.js +67 -0
- package/dist/cli/info.d.ts +2 -0
- package/dist/cli/info.js +87 -0
- package/dist/cli/list.d.ts +2 -0
- package/dist/cli/list.js +71 -0
- package/dist/cli/locate.d.ts +2 -0
- package/dist/cli/locate.js +64 -0
- package/dist/cli/lookup.d.ts +2 -0
- package/dist/cli/lookup.js +128 -0
- package/dist/cli/search.d.ts +2 -0
- package/dist/cli/search.js +119 -0
- package/dist/cli/shared.d.ts +50 -0
- package/dist/cli/shared.js +140 -0
- package/dist/cli/show.d.ts +2 -0
- package/dist/cli/show.js +38 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +107 -0
- package/dist/container.d.ts +17 -0
- package/dist/container.js +68 -0
- package/dist/directory.d.ts +21 -0
- package/dist/directory.js +31 -0
- package/dist/errors.d.ts +52 -0
- package/dist/errors.js +56 -0
- package/dist/format.d.ts +21 -0
- package/dist/format.js +22 -0
- package/dist/header.d.ts +29 -0
- package/dist/header.js +69 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +18 -0
- package/dist/indexes.d.ts +11 -0
- package/dist/indexes.js +12 -0
- package/dist/inflate.d.ts +21 -0
- package/dist/inflate.js +61 -0
- package/dist/locateIndex.d.ts +142 -0
- package/dist/locateIndex.js +561 -0
- package/dist/morton.d.ts +28 -0
- package/dist/morton.js +51 -0
- package/dist/node.d.ts +65 -0
- package/dist/node.js +137 -0
- package/dist/placeTable.d.ts +69 -0
- package/dist/placeTable.js +177 -0
- package/dist/reader.d.ts +534 -0
- package/dist/reader.js +1128 -0
- package/dist/relevance.d.ts +184 -0
- package/dist/relevance.js +266 -0
- package/dist/searchIndex.d.ts +148 -0
- package/dist/searchIndex.js +786 -0
- package/dist/source.d.ts +49 -0
- package/dist/source.js +50 -0
- package/dist/turns.d.ts +19 -0
- package/dist/turns.js +40 -0
- package/dist/varint.d.ts +52 -0
- package/dist/varint.js +137 -0
- 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
|