@mailwoman/resolver-wof-sqlite 2.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/README.md +250 -0
- package/out/address-point-interpolation.d.ts +48 -0
- package/out/address-point-interpolation.d.ts.map +1 -0
- package/out/address-point-interpolation.js +164 -0
- package/out/address-point-interpolation.js.map +1 -0
- package/out/address-point-schema.d.ts +58 -0
- package/out/address-point-schema.d.ts.map +1 -0
- package/out/address-point-schema.js +67 -0
- package/out/address-point-schema.js.map +1 -0
- package/out/address-point.d.ts +29 -0
- package/out/address-point.d.ts.map +1 -0
- package/out/address-point.js +62 -0
- package/out/address-point.js.map +1 -0
- package/out/ancestry.d.ts +40 -0
- package/out/ancestry.d.ts.map +1 -0
- package/out/ancestry.js +53 -0
- package/out/ancestry.js.map +1 -0
- package/out/build-candidate-cli.d.ts +16 -0
- package/out/build-candidate-cli.d.ts.map +1 -0
- package/out/build-candidate-cli.js +80 -0
- package/out/build-candidate-cli.js.map +1 -0
- package/out/build-candidate.d.ts +54 -0
- package/out/build-candidate.d.ts.map +1 -0
- package/out/build-candidate.js +230 -0
- package/out/build-candidate.js.map +1 -0
- package/out/build-coincident-roles-cli.d.ts +16 -0
- package/out/build-coincident-roles-cli.d.ts.map +1 -0
- package/out/build-coincident-roles-cli.js +94 -0
- package/out/build-coincident-roles-cli.js.map +1 -0
- package/out/build-fts-cli.d.ts +23 -0
- package/out/build-fts-cli.d.ts.map +1 -0
- package/out/build-fts-cli.js +117 -0
- package/out/build-fts-cli.js.map +1 -0
- package/out/build-slim-cli.d.ts +14 -0
- package/out/build-slim-cli.d.ts.map +1 -0
- package/out/build-slim-cli.js +130 -0
- package/out/build-slim-cli.js.map +1 -0
- package/out/build-slim.d.ts +71 -0
- package/out/build-slim.d.ts.map +1 -0
- package/out/build-slim.js +267 -0
- package/out/build-slim.js.map +1 -0
- package/out/candidate-lookup.d.ts +43 -0
- package/out/candidate-lookup.d.ts.map +1 -0
- package/out/candidate-lookup.js +191 -0
- package/out/candidate-lookup.js.map +1 -0
- package/out/candidate-schema.d.ts +86 -0
- package/out/candidate-schema.d.ts.map +1 -0
- package/out/candidate-schema.js +109 -0
- package/out/candidate-schema.js.map +1 -0
- package/out/coincident-roles.d.ts +86 -0
- package/out/coincident-roles.d.ts.map +1 -0
- package/out/coincident-roles.js +160 -0
- package/out/coincident-roles.js.map +1 -0
- package/out/convention.d.ts +109 -0
- package/out/convention.d.ts.map +1 -0
- package/out/convention.js +94 -0
- package/out/convention.js.map +1 -0
- package/out/fst-autocomplete.d.ts +49 -0
- package/out/fst-autocomplete.d.ts.map +1 -0
- package/out/fst-autocomplete.js +124 -0
- package/out/fst-autocomplete.js.map +1 -0
- package/out/fst-builder.d.ts +20 -0
- package/out/fst-builder.d.ts.map +1 -0
- package/out/fst-builder.js +219 -0
- package/out/fst-builder.js.map +1 -0
- package/out/fst-deserialize-web.d.ts +16 -0
- package/out/fst-deserialize-web.d.ts.map +1 -0
- package/out/fst-deserialize-web.js +133 -0
- package/out/fst-deserialize-web.js.map +1 -0
- package/out/fst-matcher.d.ts +33 -0
- package/out/fst-matcher.d.ts.map +1 -0
- package/out/fst-matcher.js +117 -0
- package/out/fst-matcher.js.map +1 -0
- package/out/fst-serialize.d.ts +30 -0
- package/out/fst-serialize.d.ts.map +1 -0
- package/out/fst-serialize.js +261 -0
- package/out/fst-serialize.js.map +1 -0
- package/out/fst-types.d.ts +60 -0
- package/out/fst-types.d.ts.map +1 -0
- package/out/fst-types.js +11 -0
- package/out/fst-types.js.map +1 -0
- package/out/fts.d.ts +158 -0
- package/out/fts.d.ts.map +1 -0
- package/out/fts.js +261 -0
- package/out/fts.js.map +1 -0
- package/out/geo.d.ts +74 -0
- package/out/geo.d.ts.map +1 -0
- package/out/geo.js +88 -0
- package/out/geo.js.map +1 -0
- package/out/index.d.ts +27 -0
- package/out/index.d.ts.map +1 -0
- package/out/index.js +22 -0
- package/out/index.js.map +1 -0
- package/out/interpolation.d.ts +84 -0
- package/out/interpolation.d.ts.map +1 -0
- package/out/interpolation.js +150 -0
- package/out/interpolation.js.map +1 -0
- package/out/lookup.d.ts +156 -0
- package/out/lookup.d.ts.map +1 -0
- package/out/lookup.js +876 -0
- package/out/lookup.js.map +1 -0
- package/out/postal-city-alias-lookup.d.ts +50 -0
- package/out/postal-city-alias-lookup.d.ts.map +1 -0
- package/out/postal-city-alias-lookup.js +66 -0
- package/out/postal-city-alias-lookup.js.map +1 -0
- package/out/postal-city-alias-schema.d.ts +51 -0
- package/out/postal-city-alias-schema.d.ts.map +1 -0
- package/out/postal-city-alias-schema.js +47 -0
- package/out/postal-city-alias-schema.js.map +1 -0
- package/out/postal-city-candidate-schema.d.ts +58 -0
- package/out/postal-city-candidate-schema.d.ts.map +1 -0
- package/out/postal-city-candidate-schema.js +56 -0
- package/out/postal-city-candidate-schema.js.map +1 -0
- package/out/postcode-point-lookup.d.ts +38 -0
- package/out/postcode-point-lookup.d.ts.map +1 -0
- package/out/postcode-point-lookup.js +46 -0
- package/out/postcode-point-lookup.js.map +1 -0
- package/out/reverse.d.ts +99 -0
- package/out/reverse.d.ts.map +1 -0
- package/out/reverse.js +290 -0
- package/out/reverse.js.map +1 -0
- package/out/schema.d.ts +163 -0
- package/out/schema.d.ts.map +1 -0
- package/out/schema.js +18 -0
- package/out/schema.js.map +1 -0
- package/out/sharding.d.ts +96 -0
- package/out/sharding.d.ts.map +1 -0
- package/out/sharding.js +129 -0
- package/out/sharding.js.map +1 -0
- package/out/sqlite-convention-source.d.ts +29 -0
- package/out/sqlite-convention-source.d.ts.map +1 -0
- package/out/sqlite-convention-source.js +53 -0
- package/out/sqlite-convention-source.js.map +1 -0
- package/out/sqlite-utils.d.ts +17 -0
- package/out/sqlite-utils.d.ts.map +1 -0
- package/out/sqlite-utils.js +24 -0
- package/out/sqlite-utils.js.map +1 -0
- package/out/street-morphology-fst-builder.d.ts +59 -0
- package/out/street-morphology-fst-builder.d.ts.map +1 -0
- package/out/street-morphology-fst-builder.js +174 -0
- package/out/street-morphology-fst-builder.js.map +1 -0
- package/out/street-normalize.d.ts +66 -0
- package/out/street-normalize.d.ts.map +1 -0
- package/out/street-normalize.js +176 -0
- package/out/street-normalize.js.map +1 -0
- package/out/street-segment-schema.d.ts +61 -0
- package/out/street-segment-schema.d.ts.map +1 -0
- package/out/street-segment-schema.js +64 -0
- package/out/street-segment-schema.js.map +1 -0
- package/out/types.d.ts +137 -0
- package/out/types.d.ts.map +1 -0
- package/out/types.js +13 -0
- package/out/types.js.map +1 -0
- package/out/unified-schema.d.ts +25 -0
- package/out/unified-schema.d.ts.map +1 -0
- package/out/unified-schema.js +142 -0
- package/out/unified-schema.js.map +1 -0
- package/package.json +54 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Build a "slim" Who's On First SQLite distribution that's small enough to ship as a static asset
|
|
7
|
+
* for the browser-side mailwoman demo (Path B of the demo plan). The full admin distribution is
|
|
8
|
+
* ~2 GB; the slim variant aims for the ~50–100 MB range by keeping only the places a public demo
|
|
9
|
+
* will actually query for.
|
|
10
|
+
*
|
|
11
|
+
* Selection policy (v1, US-focused):
|
|
12
|
+
*
|
|
13
|
+
* - All countries / regions / counties / boroughs in the configured `countries` set, so the ancestor
|
|
14
|
+
* chain a locality / postcode reports through `parent_id` stays intact.
|
|
15
|
+
* - Top-K localities by population (read from the source's pre-built `place_population` aux table) in
|
|
16
|
+
* those countries.
|
|
17
|
+
* - All postcodes in those countries — they're small and addressing-relevant.
|
|
18
|
+
* - All `names` + `place_population` rows for selected place IDs.
|
|
19
|
+
* - The `coincident_roles` dual-role relation (#402), filtered to surviving spr ids.
|
|
20
|
+
*
|
|
21
|
+
* No geojson, by design. The upstream WOF GeoJSON bodies live ONLY in the raw `whosonfirst-data-*`
|
|
22
|
+
* repos; `scripts/build-unified-wof.ts` extracts `wof:population` straight into
|
|
23
|
+
* `place_population` (and the bbox into `spr`) at ingest and never persists a `geojson` table. So
|
|
24
|
+
* the source admin DB carries population in `place_population`, and this builder consumes it
|
|
25
|
+
* directly — there is nothing to extract from, and nothing to drop.
|
|
26
|
+
*
|
|
27
|
+
* The output DB has the resolver-facing schema: `spr`, `names`, `place_population`, plus the
|
|
28
|
+
* `place_search` FTS5 / `place_bbox` R*Tree virtual tables rebuilt against the trimmed row set
|
|
29
|
+
* (both derive purely from `spr` + `names` — see `fts.ts`). That means `WofSqlitePlaceLookup`
|
|
30
|
+
* opens the slim DB without any code change — it sees a smaller universe, nothing more.
|
|
31
|
+
*
|
|
32
|
+
* Multi-shard inputs (e.g. admin + postcode) are processed in sequence; selected rows accumulate
|
|
33
|
+
* into the single output DB. The postcode shard contributes only postcodes; admin contributes
|
|
34
|
+
* everything else. Empty / missing input paths are skipped (callers pass `""` when a shard, such
|
|
35
|
+
* as a custom postcode DB, isn't built yet).
|
|
36
|
+
*/
|
|
37
|
+
import { SqliteDialect } from "@mailwoman/core/kysley/dialect";
|
|
38
|
+
import { Kysely, sql } from "kysely";
|
|
39
|
+
import { copyFileSync, existsSync, mkdtempSync, rmSync, statSync } from "node:fs";
|
|
40
|
+
import { tmpdir } from "node:os";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
import { DatabaseSync } from "node:sqlite";
|
|
43
|
+
import { buildPlaceSearchFts, PLACE_BBOX_TABLE, PLACE_POPULATION_TABLE, PLACE_SEARCH_TABLE } from "./fts.js";
|
|
44
|
+
/** Placetypes that we always keep so the ancestor chain a selected locality reports stays valid. */
|
|
45
|
+
const ANCESTOR_PLACETYPES = ["country", "region", "county", "borough", "macroregion"];
|
|
46
|
+
/** Tables copied verbatim (schema + filtered rows) from each source DB. Anything else is dropped. */
|
|
47
|
+
const COPIED_TABLES = ["spr", "names", PLACE_POPULATION_TABLE];
|
|
48
|
+
/** Fallback DDL for `place_population` when the first source predates the aux table (defensive). */
|
|
49
|
+
const PLACE_POPULATION_DDL = `CREATE TABLE ${PLACE_POPULATION_TABLE} (id INTEGER PRIMARY KEY, population INTEGER NOT NULL DEFAULT 0)`;
|
|
50
|
+
export async function buildSlimWofDatabase(opts) {
|
|
51
|
+
const countries = (opts.countries ?? ["US"]).map((c) => c.toUpperCase());
|
|
52
|
+
const topLocalities = opts.topLocalitiesPerCountry ?? 1000;
|
|
53
|
+
const progress = opts.onProgress ?? (() => { });
|
|
54
|
+
// Callers pass `""` for shards that don't exist yet (e.g. a not-yet-built custom postcode DB).
|
|
55
|
+
// Skip empties up front; require every remaining path to exist.
|
|
56
|
+
const inputs = opts.inputs.filter((p) => p.length > 0);
|
|
57
|
+
if (inputs.length === 0)
|
|
58
|
+
throw new Error("no input WOF dbs provided");
|
|
59
|
+
for (const input of inputs) {
|
|
60
|
+
if (!existsSync(input))
|
|
61
|
+
throw new Error(`input WOF db not found: ${input}`);
|
|
62
|
+
}
|
|
63
|
+
progress("init", `${inputs.length} input(s) → ${opts.output}`);
|
|
64
|
+
if (existsSync(opts.output))
|
|
65
|
+
rmSync(opts.output);
|
|
66
|
+
// Open the output DB and create the empty schema. We discover the schema from the FIRST input
|
|
67
|
+
// (raw sqlite_master read — Kysely doesn't model that) so the output mirrors source column
|
|
68
|
+
// ordering / types. `CREATE TABLE AS SELECT` flattens types to dynamic, which would break
|
|
69
|
+
// callers that rely on column-affinity behavior.
|
|
70
|
+
const out = new DatabaseSync(opts.output);
|
|
71
|
+
try {
|
|
72
|
+
const firstSource = new DatabaseSync(inputs[0], { readOnly: true });
|
|
73
|
+
try {
|
|
74
|
+
progress("schema", "copying spr / names / place_population schemas from first input");
|
|
75
|
+
for (const table of COPIED_TABLES) {
|
|
76
|
+
const createSql = firstSource
|
|
77
|
+
.prepare(`SELECT sql FROM sqlite_master WHERE type = 'table' AND name = ?`)
|
|
78
|
+
.get(table);
|
|
79
|
+
if (createSql?.sql) {
|
|
80
|
+
// Raw DDL by design (introspect-and-replay): we exec the SOURCE DB's own CREATE TABLE
|
|
81
|
+
// string read from sqlite_master, so a static Kysely builder can't express it. See AGENTS.md.
|
|
82
|
+
out.exec(createSql.sql);
|
|
83
|
+
}
|
|
84
|
+
else if (table === PLACE_POPULATION_TABLE) {
|
|
85
|
+
// Older source builds may predate the aux table — create it empty so the per-source
|
|
86
|
+
// copy + ranking have somewhere to land. Sparse-by-design; missing rows are fine.
|
|
87
|
+
out.exec(PLACE_POPULATION_DDL);
|
|
88
|
+
}
|
|
89
|
+
else {
|
|
90
|
+
throw new Error(`source DB ${inputs[0]} is missing required table '${table}'`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// PRIMARY KEY on spr.id + place_population.id come from the schemas we copied; an explicit
|
|
94
|
+
// index on names.id helps the per-id INSERT SELECT later.
|
|
95
|
+
out.exec(`CREATE INDEX IF NOT EXISTS names_id_idx ON names(id);`);
|
|
96
|
+
}
|
|
97
|
+
finally {
|
|
98
|
+
firstSource.close();
|
|
99
|
+
}
|
|
100
|
+
const kysely = new Kysely({ dialect: new SqliteDialect({ database: out }) });
|
|
101
|
+
// Pull rows from each input.
|
|
102
|
+
for (const inputPath of inputs) {
|
|
103
|
+
await copyFromSource(out, kysely, inputPath, countries, topLocalities, progress);
|
|
104
|
+
}
|
|
105
|
+
// Build the resolver virtual tables on the trimmed row set. Both place_search (FTS5) and
|
|
106
|
+
// place_bbox (R*Tree) derive purely from spr + names — no geojson needed (see fts.ts). The
|
|
107
|
+
// population aux table is NOT rebuilt here: it was copied verbatim above, and fts.ts only
|
|
108
|
+
// (re)builds it when a `geojson` table is present, which the slim DB intentionally has not.
|
|
109
|
+
progress("fts", "building place_search / place_bbox on slim DB");
|
|
110
|
+
buildPlaceSearchFts(out, {
|
|
111
|
+
drop: true, // schema we copied had no FTS tables, but be explicit
|
|
112
|
+
onProgress: (phase, name) => progress("fts", `${phase} ${name}`),
|
|
113
|
+
});
|
|
114
|
+
// Materialize region/state ABBREVIATIONS into a standalone `place_abbr (id, abbr)` table BEFORE
|
|
115
|
+
// `names` is (optionally) dropped. The full DB lets the resolver tier an exact-abbrev match by
|
|
116
|
+
// querying `names` (`#exactMatchIds`), but the slim DB drops `names` for size — so the
|
|
117
|
+
// browser resolver gets its own tiny lookup (~hundreds of rows) to do the same data-driven
|
|
118
|
+
// exact-abbrev tiering ("VT" → Vermont, not a token-matching foreign region) instead of the
|
|
119
|
+
// demo's hardcoded `expandUsRegion` map. Sourced from the `language='abbr'` rows
|
|
120
|
+
// `add-region-abbrevs.ts` wrote, already filtered to surviving spr ids via the names copy. The
|
|
121
|
+
// table is always created (empty when the source predates the abbrev enrichment) so the
|
|
122
|
+
// resolver can query it unconditionally.
|
|
123
|
+
progress("place_abbr", "materializing region abbreviations");
|
|
124
|
+
out.exec(`CREATE TABLE IF NOT EXISTS place_abbr (id INTEGER NOT NULL, abbr TEXT NOT NULL)`);
|
|
125
|
+
out.exec(`INSERT INTO place_abbr (id, abbr) SELECT id, name FROM names WHERE language = 'abbr'`);
|
|
126
|
+
out.exec(`CREATE INDEX IF NOT EXISTS place_abbr_by_abbr ON place_abbr (abbr COLLATE NOCASE)`);
|
|
127
|
+
out.exec(`CREATE INDEX IF NOT EXISTS place_abbr_by_id ON place_abbr (id)`);
|
|
128
|
+
// Capture the names count BEFORE any drop so the build report stays informative.
|
|
129
|
+
const namesRows = countRows(out, "names");
|
|
130
|
+
// Optionally drop `names` (+ its index) now that the self-contained FTS5 index no longer needs
|
|
131
|
+
// it. The resolver never reads `names` at query time, so this is pure size reduction.
|
|
132
|
+
if (opts.dropNames) {
|
|
133
|
+
progress("vacuum", `dropping names table (${namesRows} rows; FTS5 is self-contained)`);
|
|
134
|
+
out.exec(`DROP INDEX IF EXISTS names_id_idx;`);
|
|
135
|
+
out.exec(`DROP TABLE IF EXISTS names;`);
|
|
136
|
+
}
|
|
137
|
+
// VACUUM the output so the on-disk file reflects just the trimmed row count. Without it the
|
|
138
|
+
// file size stays inflated from the in-flight INSERT churn.
|
|
139
|
+
progress("vacuum", "VACUUM (final size reduction)");
|
|
140
|
+
out.exec("VACUUM;");
|
|
141
|
+
const rowCounts = {
|
|
142
|
+
spr: countRows(out, "spr"),
|
|
143
|
+
names: namesRows,
|
|
144
|
+
placeSearch: countRows(out, PLACE_SEARCH_TABLE),
|
|
145
|
+
placeBbox: countRows(out, PLACE_BBOX_TABLE),
|
|
146
|
+
placePopulation: countRows(out, PLACE_POPULATION_TABLE),
|
|
147
|
+
};
|
|
148
|
+
progress("done", JSON.stringify(rowCounts));
|
|
149
|
+
return {
|
|
150
|
+
outputPath: opts.output,
|
|
151
|
+
outputBytes: statSync(opts.output).size,
|
|
152
|
+
rowCounts,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
out.close();
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
async function copyFromSource(out, kysely, inputPath, countries, topLocalities, progress) {
|
|
160
|
+
// ATTACH avoids any "load source into memory" step — SQLite walks both files in place. We need
|
|
161
|
+
// a fresh temp copy because some WOF distributions ship as read-only filesystem mounts and
|
|
162
|
+
// ATTACH will still want a writable journal on the side; copying to /tmp dodges that without
|
|
163
|
+
// mutating the canonical files in /mnt/playpen/mailwoman-data/wof/. ATTACH / DETACH stay raw
|
|
164
|
+
// — Kysely doesn't model them.
|
|
165
|
+
const tmpScratch = mkdtempSync(join(tmpdir(), "mailwoman-slim-src-"));
|
|
166
|
+
const scratchPath = join(tmpScratch, "src.db");
|
|
167
|
+
copyFileSync(inputPath, scratchPath);
|
|
168
|
+
try {
|
|
169
|
+
out.exec(`ATTACH DATABASE '${scratchPath.replace(/'/g, "''")}' AS src;`);
|
|
170
|
+
try {
|
|
171
|
+
// Does this shard carry the pre-built population aux table? The admin source does; a bare
|
|
172
|
+
// postcode shard might not. The locality ranking + population copy below adapt accordingly.
|
|
173
|
+
const srcHasPopulation = Boolean(out.prepare(`SELECT 1 FROM src.sqlite_master WHERE type = 'table' AND name = '${PLACE_POPULATION_TABLE}'`).get());
|
|
174
|
+
// The SELECT-INSERT queries below go through Kysely. The cross-schema FROM is the only
|
|
175
|
+
// "interesting" bit: by declaring `src.spr` / `src.names` / `src.place_population` in
|
|
176
|
+
// `BuildSchema`, Kysely lets us write `selectFrom("src.spr")` with the same column-type
|
|
177
|
+
// checking as the regular schema. SQLite parses the dotted identifier as a schema-name
|
|
178
|
+
// qualifier, so this works directly without any aliasing trick.
|
|
179
|
+
// 1. Ancestor placetypes (country / region / county / etc.) — always-kept.
|
|
180
|
+
progress("country", `${inputPath}: ancestor placetypes in (${countries.join(",")})`);
|
|
181
|
+
await kysely
|
|
182
|
+
.insertInto("spr")
|
|
183
|
+
.expression((eb) => eb
|
|
184
|
+
.selectFrom("src.spr")
|
|
185
|
+
.selectAll()
|
|
186
|
+
.where("is_current", "!=", 0)
|
|
187
|
+
.where("is_deprecated", "=", 0)
|
|
188
|
+
.where("country", "in", countries)
|
|
189
|
+
.where("placetype", "in", [...ANCESTOR_PLACETYPES]))
|
|
190
|
+
.onConflict((oc) => oc.doNothing())
|
|
191
|
+
.execute();
|
|
192
|
+
// 2. Top-K localities by population. Population lives in the pre-built `place_population`
|
|
193
|
+
// aux table — left-join it so localities without a population row still qualify (sorted
|
|
194
|
+
// last). If the shard has no population table, fall back to a deterministic id ordering.
|
|
195
|
+
progress("locality", `${inputPath}: top-${topLocalities} localities by population`);
|
|
196
|
+
await kysely
|
|
197
|
+
.insertInto("spr")
|
|
198
|
+
.expression((eb) => eb
|
|
199
|
+
.selectFrom("src.spr as s")
|
|
200
|
+
.$if(srcHasPopulation, (qb) => qb.leftJoin("src.place_population as p", "p.id", "s.id"))
|
|
201
|
+
.selectAll("s")
|
|
202
|
+
.where("s.is_current", "!=", 0)
|
|
203
|
+
.where("s.is_deprecated", "=", 0)
|
|
204
|
+
.where("s.country", "in", countries)
|
|
205
|
+
.where("s.placetype", "=", "locality")
|
|
206
|
+
.orderBy(srcHasPopulation ? sql `COALESCE(p.population, 0)` : sql `s.id`, "desc")
|
|
207
|
+
.limit(topLocalities))
|
|
208
|
+
.onConflict((oc) => oc.doNothing())
|
|
209
|
+
.execute();
|
|
210
|
+
// 3. All postcodes in scope.
|
|
211
|
+
progress("postcode", `${inputPath}: all postcodes`);
|
|
212
|
+
await kysely
|
|
213
|
+
.insertInto("spr")
|
|
214
|
+
.expression((eb) => eb
|
|
215
|
+
.selectFrom("src.spr")
|
|
216
|
+
.selectAll()
|
|
217
|
+
.where("is_current", "!=", 0)
|
|
218
|
+
.where("is_deprecated", "=", 0)
|
|
219
|
+
.where("country", "in", countries)
|
|
220
|
+
.where("placetype", "=", "postalcode"))
|
|
221
|
+
.onConflict((oc) => oc.doNothing())
|
|
222
|
+
.execute();
|
|
223
|
+
// 4. Pull names for the IDs we just selected.
|
|
224
|
+
progress("names", `${inputPath}: names rows for selected IDs`);
|
|
225
|
+
await kysely
|
|
226
|
+
.insertInto("names")
|
|
227
|
+
.expression((eb) => eb.selectFrom("src.names").selectAll().where("id", "in", eb.selectFrom("spr").select("id")))
|
|
228
|
+
.onConflict((oc) => oc.doNothing())
|
|
229
|
+
.execute();
|
|
230
|
+
// 5. Pull population rows for the selected IDs (sparse — only the places WOF has a count for).
|
|
231
|
+
if (srcHasPopulation) {
|
|
232
|
+
progress("place_population", `${inputPath}: population rows for selected IDs`);
|
|
233
|
+
await kysely
|
|
234
|
+
.insertInto("place_population")
|
|
235
|
+
.expression((eb) => eb.selectFrom("src.place_population").selectAll().where("id", "in", eb.selectFrom("spr").select("id")))
|
|
236
|
+
.onConflict((oc) => oc.doNothing())
|
|
237
|
+
.execute();
|
|
238
|
+
}
|
|
239
|
+
// 6. Carry the coincident_roles relation (#402) when this source has it (the admin DB), so the
|
|
240
|
+
// slim/demo DB supports dual-role hierarchy completion (on by default). Filtered to surviving
|
|
241
|
+
// spr ids → no orphans. Tiny (~hundreds of rows). `ancestors` is intentionally NOT copied (huge
|
|
242
|
+
// + build-only), so we copy the derived table rather than rebuild it. Raw SQL — conditional +
|
|
243
|
+
// not in the Kysely build schema.
|
|
244
|
+
const relationSchema = out
|
|
245
|
+
.prepare(`SELECT sql FROM src.sqlite_master WHERE type = 'table' AND name = 'coincident_roles'`)
|
|
246
|
+
.get();
|
|
247
|
+
if (relationSchema?.sql) {
|
|
248
|
+
progress("coincident_roles", `${inputPath}: copying dual-role relation`);
|
|
249
|
+
out.exec(relationSchema.sql.replace(/CREATE TABLE/i, "CREATE TABLE IF NOT EXISTS"));
|
|
250
|
+
out.exec(`INSERT OR IGNORE INTO coincident_roles SELECT * FROM src.coincident_roles
|
|
251
|
+
WHERE admin_id IN (SELECT id FROM spr) AND locality_id IN (SELECT id FROM spr)`);
|
|
252
|
+
out.exec(`CREATE INDEX IF NOT EXISTS coincident_roles_by_admin ON coincident_roles (admin_id)`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
finally {
|
|
256
|
+
out.exec(`DETACH DATABASE src;`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
finally {
|
|
260
|
+
rmSync(tmpScratch, { recursive: true, force: true });
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function countRows(db, table) {
|
|
264
|
+
const row = db.prepare(`SELECT COUNT(*) AS n FROM ${table}`).get();
|
|
265
|
+
return Number(row?.n ?? 0);
|
|
266
|
+
}
|
|
267
|
+
//# sourceMappingURL=build-slim.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-slim.js","sourceRoot":"","sources":["../build-slim.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Node-side {@link PlaceLookup} over the byte-range CANDIDATE table (`build-candidate.ts`) — the
|
|
7
|
+
* SAME gazetteer the browser demo resolves against ({@link WofCandidateTableLookup} in
|
|
8
|
+
* `docs/src/shared/httpvfs-resolver.ts`), but reading a LOCAL `candidate.db` via `node:sqlite`
|
|
9
|
+
* instead of sql.js-httpvfs. This is what makes the server/CLI resolver match the demo: one
|
|
10
|
+
* lookup surface, one artifact, one ranking.
|
|
11
|
+
*
|
|
12
|
+
* The query is a single contiguous probe on the `WITHOUT ROWID` B-tree keyed `(name_key,
|
|
13
|
+
* country_id, region_id, placetype_id, neg_rank, spr_id)`. `name_key` is the SHARED
|
|
14
|
+
* {@link normalizeLocalityForKey} (build- and query-consistent), each row is denormalized (display
|
|
15
|
+
* `name`, centroid, bbox), and population rank is precomputed into `neg_rank` — so the result is
|
|
16
|
+
* POPULATION-FIRST and COUNTRY-AGNOSTIC (when no `country` filter is given), exactly like the
|
|
17
|
+
* demo. That's the deliberate divergence from {@link WofSqlitePlaceLookup}'s FTS/bm25 ranking: a
|
|
18
|
+
* bare "Moscow" resolves to the 10.4 M-pop Russian city, not whichever same-name US township bm25
|
|
19
|
+
* floats to the top.
|
|
20
|
+
*
|
|
21
|
+
* Disambiguation rides the same mechanism the cascade already uses: a parsed region resolves to its
|
|
22
|
+
* stored bbox and the locality query is point-in-bbox-filtered on the candidate centroid (the
|
|
23
|
+
* `bbox` field on {@link FindPlaceQuery}).
|
|
24
|
+
*/
|
|
25
|
+
import { DatabaseSync } from "node:sqlite";
|
|
26
|
+
import type { FindPlaceQuery, PlaceCandidate, PlaceLookup } from "./types.js";
|
|
27
|
+
export interface WofCandidateTableLookupOpts {
|
|
28
|
+
/** Path to a `candidate.db` built by `build-candidate.ts`. Opened read-only. */
|
|
29
|
+
databasePath?: string;
|
|
30
|
+
/** Pre-opened handle (tests / shared connections). Mutually exclusive with `databasePath`. */
|
|
31
|
+
database?: DatabaseSync;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Node {@link PlaceLookup} over `candidate.db`. Drop-in for {@link WofSqlitePlaceLookup} in
|
|
35
|
+
* `createWofResolver(backend)` — same `findPlace` contract, population-first ranking.
|
|
36
|
+
*/
|
|
37
|
+
export declare class WofCandidateTableLookup implements PlaceLookup {
|
|
38
|
+
#private;
|
|
39
|
+
constructor(opts: WofCandidateTableLookupOpts);
|
|
40
|
+
findPlace(query: FindPlaceQuery): Promise<PlaceCandidate[]>;
|
|
41
|
+
close(): void;
|
|
42
|
+
}
|
|
43
|
+
//# sourceMappingURL=candidate-lookup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidate-lookup.d.ts","sourceRoot":"","sources":["../candidate-lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAGH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAK1C,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,WAAW,EAAgB,MAAM,YAAY,CAAA;AAE3F,MAAM,WAAW,2BAA2B;IAC3C,gFAAgF;IAChF,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,8FAA8F;IAC9F,QAAQ,CAAC,EAAE,YAAY,CAAA;CACvB;AAsBD;;;GAGG;AACH,qBAAa,uBAAwB,YAAW,WAAW;;gBAc9C,IAAI,EAAE,2BAA2B;IAyCvC,SAAS,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IA0GjE,KAAK,IAAI,IAAI;CAGb"}
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Node-side {@link PlaceLookup} over the byte-range CANDIDATE table (`build-candidate.ts`) — the
|
|
7
|
+
* SAME gazetteer the browser demo resolves against ({@link WofCandidateTableLookup} in
|
|
8
|
+
* `docs/src/shared/httpvfs-resolver.ts`), but reading a LOCAL `candidate.db` via `node:sqlite`
|
|
9
|
+
* instead of sql.js-httpvfs. This is what makes the server/CLI resolver match the demo: one
|
|
10
|
+
* lookup surface, one artifact, one ranking.
|
|
11
|
+
*
|
|
12
|
+
* The query is a single contiguous probe on the `WITHOUT ROWID` B-tree keyed `(name_key,
|
|
13
|
+
* country_id, region_id, placetype_id, neg_rank, spr_id)`. `name_key` is the SHARED
|
|
14
|
+
* {@link normalizeLocalityForKey} (build- and query-consistent), each row is denormalized (display
|
|
15
|
+
* `name`, centroid, bbox), and population rank is precomputed into `neg_rank` — so the result is
|
|
16
|
+
* POPULATION-FIRST and COUNTRY-AGNOSTIC (when no `country` filter is given), exactly like the
|
|
17
|
+
* demo. That's the deliberate divergence from {@link WofSqlitePlaceLookup}'s FTS/bm25 ranking: a
|
|
18
|
+
* bare "Moscow" resolves to the 10.4 M-pop Russian city, not whichever same-name US township bm25
|
|
19
|
+
* floats to the top.
|
|
20
|
+
*
|
|
21
|
+
* Disambiguation rides the same mechanism the cascade already uses: a parsed region resolves to its
|
|
22
|
+
* stored bbox and the locality query is point-in-bbox-filtered on the candidate centroid (the
|
|
23
|
+
* `bbox` field on {@link FindPlaceQuery}).
|
|
24
|
+
*/
|
|
25
|
+
import { expandPlacetypeFilter } from "@mailwoman/resolver";
|
|
26
|
+
import { DatabaseSync } from "node:sqlite";
|
|
27
|
+
import { POSTAL_CITY_CANDIDATE_TABLE } from "./postal-city-candidate-schema.js";
|
|
28
|
+
import { hasTable } from "./sqlite-utils.js";
|
|
29
|
+
import { normalizeLocalityForKey, stripLocalityQualifier } from "./street-normalize.js";
|
|
30
|
+
/**
|
|
31
|
+
* Node {@link PlaceLookup} over `candidate.db`. Drop-in for {@link WofSqlitePlaceLookup} in
|
|
32
|
+
* `createWofResolver(backend)` — same `findPlace` contract, population-first ranking.
|
|
33
|
+
*/
|
|
34
|
+
export class WofCandidateTableLookup {
|
|
35
|
+
#db;
|
|
36
|
+
#ownsDb;
|
|
37
|
+
#countryToId = new Map();
|
|
38
|
+
#idToCountry = new Map();
|
|
39
|
+
#placetypeToId = new Map();
|
|
40
|
+
#idToPlacetype = new Map();
|
|
41
|
+
/**
|
|
42
|
+
* Prepared `(name_key, postcode)` probe for the #741 postal-city side-index — `undefined` when
|
|
43
|
+
* the `postal_city_candidate` table isn't present, so a candidate.db built without it is
|
|
44
|
+
* byte-stable.
|
|
45
|
+
*/
|
|
46
|
+
#postalCityProbe;
|
|
47
|
+
constructor(opts) {
|
|
48
|
+
if (opts.database) {
|
|
49
|
+
this.#db = opts.database;
|
|
50
|
+
this.#ownsDb = false;
|
|
51
|
+
}
|
|
52
|
+
else if (opts.databasePath) {
|
|
53
|
+
this.#db = new DatabaseSync(opts.databasePath, { readOnly: true });
|
|
54
|
+
this.#ownsDb = true;
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
throw new Error("WofCandidateTableLookup needs `databasePath` or `database`");
|
|
58
|
+
}
|
|
59
|
+
// The code tables are tiny (country/placetype dictionaries) — load them once at construction so
|
|
60
|
+
// `findPlace` is a single B-tree probe with no dictionary round-trip.
|
|
61
|
+
for (const r of this.#db.prepare("SELECT id, code FROM country_codes").all()) {
|
|
62
|
+
const code = String(r.code).toUpperCase();
|
|
63
|
+
this.#countryToId.set(code, Number(r.id));
|
|
64
|
+
this.#idToCountry.set(Number(r.id), code);
|
|
65
|
+
}
|
|
66
|
+
for (const r of this.#db
|
|
67
|
+
.prepare("SELECT id, placetype FROM placetype_codes")
|
|
68
|
+
.all()) {
|
|
69
|
+
this.#placetypeToId.set(String(r.placetype), Number(r.id));
|
|
70
|
+
this.#idToPlacetype.set(Number(r.id), String(r.placetype));
|
|
71
|
+
}
|
|
72
|
+
// #741 postal-city side-index: prepare the exact probe only if the table is present. Absent →
|
|
73
|
+
// `#postalCityProbe` stays undefined → findPlace skips the postal-city path → byte-stable.
|
|
74
|
+
if (hasTable(this.#db, POSTAL_CITY_CANDIDATE_TABLE)) {
|
|
75
|
+
this.#postalCityProbe = this.#db.prepare(`SELECT spr_id, name, latitude, longitude FROM ${POSTAL_CITY_CANDIDATE_TABLE} WHERE name_key = ? AND postcode = ? LIMIT 1`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Does this query want a locality-tier place? Postal-city aliases (#741) are all localities. */
|
|
79
|
+
#wantsLocality(placetype) {
|
|
80
|
+
if (!placetype)
|
|
81
|
+
return true;
|
|
82
|
+
const want = Array.isArray(placetype) ? placetype : [placetype];
|
|
83
|
+
return expandPlacetypeFilter(want).includes("locality");
|
|
84
|
+
}
|
|
85
|
+
async findPlace(query) {
|
|
86
|
+
const text = (query.text ?? "").trim();
|
|
87
|
+
if (!text)
|
|
88
|
+
return [];
|
|
89
|
+
const nameKey = normalizeLocalityForKey(text);
|
|
90
|
+
if (!nameKey)
|
|
91
|
+
return [];
|
|
92
|
+
// #741: postcode-keyed postal-city alias. An exact `(name_key, postcode)` hit resolves a
|
|
93
|
+
// user-typed POSTAL city ("Antioch", 37013) to the geographic locality the postcode sits in
|
|
94
|
+
// ("Nashville"), bypassing the population/region ranking that can't see the postcode. Gated on
|
|
95
|
+
// the side-index being present, a postcode in the query, and a locality-tier request — so the
|
|
96
|
+
// common (no-postcode / non-locality) path is untouched. A hit short-circuits: the postcode is
|
|
97
|
+
// an exact, high-confidence disambiguator, so we return the single geographic locality.
|
|
98
|
+
if (query.postcode && this.#postalCityProbe && this.#wantsLocality(query.placetype)) {
|
|
99
|
+
const hit = this.#postalCityProbe.get(nameKey, query.postcode.trim());
|
|
100
|
+
if (hit) {
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
id: Number(hit.spr_id),
|
|
104
|
+
name: String(hit.name ?? ""),
|
|
105
|
+
placetype: "locality",
|
|
106
|
+
country: query.country?.toUpperCase() ?? "",
|
|
107
|
+
lat: Number(hit.latitude),
|
|
108
|
+
lon: Number(hit.longitude),
|
|
109
|
+
score: 1,
|
|
110
|
+
exactMatch: true,
|
|
111
|
+
},
|
|
112
|
+
];
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
const limit = Math.max(1, query.limit ?? 10);
|
|
116
|
+
// Filter conds shared by the exact-key + strip-fallback probes (everything but name_key).
|
|
117
|
+
const filters = [];
|
|
118
|
+
const filterParams = [];
|
|
119
|
+
if (query.country) {
|
|
120
|
+
const cid = this.#countryToId.get(query.country.toUpperCase());
|
|
121
|
+
if (cid === undefined)
|
|
122
|
+
return []; // a country the candidate table doesn't carry
|
|
123
|
+
filters.push("country_id = ?");
|
|
124
|
+
filterParams.push(cid);
|
|
125
|
+
}
|
|
126
|
+
if (query.placetype) {
|
|
127
|
+
// Shared placetype-equivalence expansion (a `locality` query must also reach borough /
|
|
128
|
+
// localadmin). `postalcode` maps to no admin placetype here → empty → no rows.
|
|
129
|
+
const want = Array.isArray(query.placetype) ? query.placetype : [query.placetype];
|
|
130
|
+
const ids = expandPlacetypeFilter(want)
|
|
131
|
+
.map((t) => this.#placetypeToId.get(t))
|
|
132
|
+
.filter((v) => v !== undefined);
|
|
133
|
+
if (ids.length === 0)
|
|
134
|
+
return [];
|
|
135
|
+
filters.push(`placetype_id IN (${ids.map(() => "?").join(",")})`);
|
|
136
|
+
filterParams.push(...ids);
|
|
137
|
+
}
|
|
138
|
+
if (query.bbox) {
|
|
139
|
+
const b = query.bbox;
|
|
140
|
+
filters.push("latitude BETWEEN ? AND ? AND longitude BETWEEN ? AND ?");
|
|
141
|
+
filterParams.push(b.minLat, b.maxLat, b.minLon, b.maxLon);
|
|
142
|
+
}
|
|
143
|
+
const probe = (nk) => {
|
|
144
|
+
const conds = ["name_key = ?", ...filters];
|
|
145
|
+
const sql = "SELECT spr_id, name, country_id, placetype_id, latitude, longitude, min_lat, min_lon, max_lat, max_lon, neg_rank " +
|
|
146
|
+
`FROM candidate WHERE ${conds.join(" AND ")} ORDER BY neg_rank ASC LIMIT ?`;
|
|
147
|
+
return this.#db.prepare(sql).all(nk, ...filterParams, limit);
|
|
148
|
+
};
|
|
149
|
+
let rows = probe(nameKey);
|
|
150
|
+
if (rows.length === 0) {
|
|
151
|
+
// Query-side qualifier-strip fallback: an OA locality with a qualifier the gazetteer's
|
|
152
|
+
// canonical name omits ("Lenk im Simmental" → "Lenk", "Roche VD"). Tried ONLY on an exact
|
|
153
|
+
// miss; the cascade's region bbox disambiguates any base-name ambiguity.
|
|
154
|
+
const strippedKey = normalizeLocalityForKey(stripLocalityQualifier(text));
|
|
155
|
+
if (strippedKey && strippedKey !== nameKey)
|
|
156
|
+
rows = probe(strippedKey);
|
|
157
|
+
}
|
|
158
|
+
return rows.map((row) => {
|
|
159
|
+
const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null;
|
|
160
|
+
return {
|
|
161
|
+
id: Number(row.spr_id),
|
|
162
|
+
name: String(row.name ?? ""),
|
|
163
|
+
placetype: (this.#idToPlacetype.get(Number(row.placetype_id)) ?? ""),
|
|
164
|
+
// Surfaced so the cascade can country-gate a postcode by the resolved locality (an ambiguous
|
|
165
|
+
// international postcode like 10115 = Berlin DE AND New York US must not out-resolve the city).
|
|
166
|
+
country: this.#idToCountry.get(Number(row.country_id)) ?? "",
|
|
167
|
+
lat: Number(row.latitude),
|
|
168
|
+
lon: Number(row.longitude),
|
|
169
|
+
score: -Number(row.neg_rank),
|
|
170
|
+
// Every candidate row IS an exact normalized-name (or alias/abbrev) match — the cascade's
|
|
171
|
+
// exact tier accepts alias-exact hits ("New York City" → New York) the same as canonical.
|
|
172
|
+
exactMatch: true,
|
|
173
|
+
...(hasBbox
|
|
174
|
+
? {
|
|
175
|
+
bbox: {
|
|
176
|
+
minLat: Number(row.min_lat),
|
|
177
|
+
maxLat: Number(row.max_lat),
|
|
178
|
+
minLon: Number(row.min_lon),
|
|
179
|
+
maxLon: Number(row.max_lon),
|
|
180
|
+
},
|
|
181
|
+
}
|
|
182
|
+
: {}),
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
close() {
|
|
187
|
+
if (this.#ownsDb)
|
|
188
|
+
this.#db.close();
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
//# sourceMappingURL=candidate-lookup.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidate-lookup.js","sourceRoot":"","sources":["../candidate-lookup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,qBAAqB,EAAE,MAAM,qBAAqB,CAAA;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAE1C,OAAO,EAAE,2BAA2B,EAAiC,MAAM,mCAAmC,CAAA;AAC9G,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,uBAAuB,EAAE,sBAAsB,EAAE,MAAM,uBAAuB,CAAA;AA8BvF;;;GAGG;AACH,MAAM,OAAO,uBAAuB;IACnC,GAAG,CAAc;IACjB,OAAO,CAAS;IACP,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAA;IACxC,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAA;IAC1C,cAAc,GAAG,IAAI,GAAG,EAAkB,CAAA;IACnD;;;;OAIG;IACM,gBAAgB,CAAiD;IAE1E,YAAY,IAAiC;QAC5C,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;YACnB,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAA;YACxB,IAAI,CAAC,OAAO,GAAG,KAAK,CAAA;QACrB,CAAC;aAAM,IAAI,IAAI,CAAC,YAAY,EAAE,CAAC;YAC9B,IAAI,CAAC,GAAG,GAAG,IAAI,YAAY,CAAC,IAAI,CAAC,YAAY,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAA;YAClE,IAAI,CAAC,OAAO,GAAG,IAAI,CAAA;QACpB,CAAC;aAAM,CAAC;YACP,MAAM,IAAI,KAAK,CAAC,4DAA4D,CAAC,CAAA;QAC9E,CAAC;QAED,gGAAgG;QAChG,sEAAsE;QACtE,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,oCAAoC,CAAC,CAAC,GAAG,EAAmC,EAAE,CAAC;YAC/G,MAAM,IAAI,GAAG,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,WAAW,EAAE,CAAA;YACzC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YACzC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,IAAI,CAAC,CAAA;QAC1C,CAAC;QACD,KAAK,MAAM,CAAC,IAAI,IAAI,CAAC,GAAG;aACtB,OAAO,CAAC,2CAA2C,CAAC;aACpD,GAAG,EAAqC,EAAE,CAAC;YAC5C,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAA;YAC1D,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAA;QAC3D,CAAC;QAED,8FAA8F;QAC9F,2FAA2F;QAC3F,IAAI,QAAQ,CAAC,IAAI,CAAC,GAAG,EAAE,2BAA2B,CAAC,EAAE,CAAC;YACrD,IAAI,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAAC,OAAO,CACvC,iDAAiD,2BAA2B,8CAA8C,CAC1H,CAAA;QACF,CAAC;IACF,CAAC;IAED,iGAAiG;IACjG,cAAc,CAAC,SAAsC;QACpD,IAAI,CAAC,SAAS;YAAE,OAAO,IAAI,CAAA;QAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAA;QAC/D,OAAO,qBAAqB,CAAC,IAAyB,CAAC,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAA;IAC7E,CAAC;IAED,KAAK,CAAC,SAAS,CAAC,KAAqB;QACpC,MAAM,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAA;QACtC,IAAI,CAAC,IAAI;YAAE,OAAO,EAAE,CAAA;QACpB,MAAM,OAAO,GAAG,uBAAuB,CAAC,IAAI,CAAC,CAAA;QAC7C,IAAI,CAAC,OAAO;YAAE,OAAO,EAAE,CAAA;QAEvB,yFAAyF;QACzF,4FAA4F;QAC5F,+FAA+F;QAC/F,8FAA8F;QAC9F,+FAA+F;QAC/F,wFAAwF;QACxF,IAAI,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC,gBAAgB,IAAI,IAAI,CAAC,cAAc,CAAC,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC;YACrF,MAAM,GAAG,GAAG,IAAI,CAAC,gBAAgB,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,CAExD,CAAA;YACZ,IAAI,GAAG,EAAE,CAAC;gBACT,OAAO;oBACN;wBACC,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;wBACtB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;wBAC5B,SAAS,EAAE,UAA0B;wBACrC,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,WAAW,EAAE,IAAI,EAAE;wBAC3C,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;wBACzB,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;wBAC1B,KAAK,EAAE,CAAC;wBACR,UAAU,EAAE,IAAI;qBAChB;iBACD,CAAA;YACF,CAAC;QACF,CAAC;QAED,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,CAAA;QAE5C,0FAA0F;QAC1F,MAAM,OAAO,GAAa,EAAE,CAAA;QAC5B,MAAM,YAAY,GAA2B,EAAE,CAAA;QAC/C,IAAI,KAAK,CAAC,OAAO,EAAE,CAAC;YACnB,MAAM,GAAG,GAAG,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,CAAA;YAC9D,IAAI,GAAG,KAAK,SAAS;gBAAE,OAAO,EAAE,CAAA,CAAC,8CAA8C;YAC/E,OAAO,CAAC,IAAI,CAAC,gBAAgB,CAAC,CAAA;YAC9B,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,CAAA;QACvB,CAAC;QACD,IAAI,KAAK,CAAC,SAAS,EAAE,CAAC;YACrB,uFAAuF;YACvF,+EAA+E;YAC/E,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,SAAS,CAAC,CAAA;YACjF,MAAM,GAAG,GAAG,qBAAqB,CAAC,IAAyB,CAAC;iBAC1D,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;iBACtC,MAAM,CAAC,CAAC,CAAC,EAAe,EAAE,CAAC,CAAC,KAAK,SAAS,CAAC,CAAA;YAC7C,IAAI,GAAG,CAAC,MAAM,KAAK,CAAC;gBAAE,OAAO,EAAE,CAAA;YAC/B,OAAO,CAAC,IAAI,CAAC,oBAAoB,GAAG,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC,CAAA;YACjE,YAAY,CAAC,IAAI,CAAC,GAAG,GAAG,CAAC,CAAA;QAC1B,CAAC;QACD,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC;YAChB,MAAM,CAAC,GAAG,KAAK,CAAC,IAAI,CAAA;YACpB,OAAO,CAAC,IAAI,CAAC,wDAAwD,CAAC,CAAA;YACtE,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAA;QAC1D,CAAC;QAED,MAAM,KAAK,GAAG,CAAC,EAAU,EAAkB,EAAE;YAC5C,MAAM,KAAK,GAAG,CAAC,cAAc,EAAE,GAAG,OAAO,CAAC,CAAA;YAC1C,MAAM,GAAG,GACR,mHAAmH;gBACnH,wBAAwB,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,gCAAgC,CAAA;YAC5E,OAAO,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,EAAE,EAAE,GAAG,YAAY,EAAE,KAAK,CAA8B,CAAA;QAC1F,CAAC,CAAA;QAED,IAAI,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,CAAA;QACzB,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACvB,uFAAuF;YACvF,0FAA0F;YAC1F,yEAAyE;YACzE,MAAM,WAAW,GAAG,uBAAuB,CAAC,sBAAsB,CAAC,IAAI,CAAC,CAAC,CAAA;YACzE,IAAI,WAAW,IAAI,WAAW,KAAK,OAAO;gBAAE,IAAI,GAAG,KAAK,CAAC,WAAW,CAAC,CAAA;QACtE,CAAC;QAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAkB,EAAE;YACvC,MAAM,OAAO,GAAG,GAAG,CAAC,OAAO,IAAI,IAAI,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,IAAI,GAAG,CAAC,OAAO,IAAI,IAAI,CAAA;YACxG,OAAO;gBACN,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,MAAM,CAAC;gBACtB,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,EAAE,CAAC;gBAC5B,SAAS,EAAE,CAAC,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,YAAY,CAAC,CAAC,IAAI,EAAE,CAAiB;gBACpF,6FAA6F;gBAC7F,gGAAgG;gBAChG,OAAO,EAAE,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE;gBAC5D,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBACzB,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC;gBAC1B,KAAK,EAAE,CAAC,MAAM,CAAC,GAAG,CAAC,QAAQ,CAAC;gBAC5B,0FAA0F;gBAC1F,0FAA0F;gBAC1F,UAAU,EAAE,IAAI;gBAChB,GAAG,CAAC,OAAO;oBACV,CAAC,CAAC;wBACA,IAAI,EAAE;4BACL,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;4BAC3B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;4BAC3B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;4BAC3B,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,OAAO,CAAC;yBAC3B;qBACD;oBACF,CAAC,CAAC,EAAE,CAAC;aACN,CAAA;QACF,CAAC,CAAC,CAAA;IACH,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,OAAO;YAAE,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAA;IACnC,CAAC;CACD"}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Typed schema for the byte-range CANDIDATE gazetteer (`candidate.db`) — the single source of truth
|
|
7
|
+
* for the columns shared by the BUILDER ({@link buildCandidateTable}) and the READERS (the Node
|
|
8
|
+
* {@link WofCandidateTableLookup} + the browser `httpvfs-resolver.ts`). Before this module each
|
|
9
|
+
* side hand-wrote the column list; a rename in one place broke the other at runtime. Now the
|
|
10
|
+
* contract is a Kysely `Database` interface (`new DatabaseClient<CandidateDatabase>(...)` for
|
|
11
|
+
* typed inserts) plus the table DDL as strings — so a column change is a compile error on every
|
|
12
|
+
* consumer.
|
|
13
|
+
*
|
|
14
|
+
* `cand_stage` is the transient staging table the builder bulk-loads; `candidate` is the clustered
|
|
15
|
+
* `WITHOUT ROWID` B-tree it's materialized into (same columns). The reader queries `candidate`.
|
|
16
|
+
*/
|
|
17
|
+
import { type Kysely } from "kysely";
|
|
18
|
+
/**
|
|
19
|
+
* One candidate row. `name_key` + the four small int keys + `neg_rank` + `spr_id` form the
|
|
20
|
+
* clustered primary key; the rest is denormalized so a resolve is one probe (no join to `spr`).
|
|
21
|
+
* Coordinates + bbox + name are nullable at the SQL level (a postcode shard row may lack a bbox).
|
|
22
|
+
*/
|
|
23
|
+
export interface CandidateTable {
|
|
24
|
+
/** The shared {@link normalizeLocalityForKey} of the name/alias — the probe key. */
|
|
25
|
+
name_key: string;
|
|
26
|
+
/** Small int from {@link CountryCodeTable} (shrinks the clustered key). */
|
|
27
|
+
country_id: number;
|
|
28
|
+
/** The place's region-tier ancestor id, or 0 (carried for the future region 2-step). */
|
|
29
|
+
region_id: number;
|
|
30
|
+
/** Small int from {@link PlacetypeCodeTable}. */
|
|
31
|
+
placetype_id: number;
|
|
32
|
+
/**
|
|
33
|
+
* `-log10(population + 1)` — ASC order = highest-population first. 0 for postcodes (no
|
|
34
|
+
* population).
|
|
35
|
+
*/
|
|
36
|
+
neg_rank: number;
|
|
37
|
+
/** WOF id of the place this row resolves to. */
|
|
38
|
+
spr_id: number;
|
|
39
|
+
name: string | null;
|
|
40
|
+
latitude: number | null;
|
|
41
|
+
longitude: number | null;
|
|
42
|
+
min_lat: number | null;
|
|
43
|
+
min_lon: number | null;
|
|
44
|
+
max_lat: number | null;
|
|
45
|
+
max_lon: number | null;
|
|
46
|
+
population: number | null;
|
|
47
|
+
/** 1 when the row is the place's canonical name (vs an alias/abbrev). */
|
|
48
|
+
is_primary: number | null;
|
|
49
|
+
}
|
|
50
|
+
/** `(id → ISO country code)` dictionary. */
|
|
51
|
+
export interface CountryCodeTable {
|
|
52
|
+
id: number;
|
|
53
|
+
code: string;
|
|
54
|
+
}
|
|
55
|
+
/** `(id → placetype)` dictionary. */
|
|
56
|
+
export interface PlacetypeCodeTable {
|
|
57
|
+
id: number;
|
|
58
|
+
placetype: string;
|
|
59
|
+
}
|
|
60
|
+
/** The candidate database schema for `new DatabaseClient<CandidateDatabase>(...)`. */
|
|
61
|
+
export interface CandidateDatabase {
|
|
62
|
+
/** The clustered `WITHOUT ROWID` lookup table the reader probes. */
|
|
63
|
+
candidate: CandidateTable;
|
|
64
|
+
/** Transient staging table (same columns); dropped once `candidate` is materialized. */
|
|
65
|
+
cand_stage: CandidateTable;
|
|
66
|
+
country_codes: CountryCodeTable;
|
|
67
|
+
placetype_codes: PlacetypeCodeTable;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The `candidate`/`cand_stage` columns in clustered-key order. The materialization `INSERT INTO
|
|
71
|
+
* candidate SELECT … FROM cand_stage` derives its column list from this, so the two tables can't
|
|
72
|
+
* drift. Keep in sync with {@link CandidateTable}.
|
|
73
|
+
*/
|
|
74
|
+
export declare const CANDIDATE_COLUMNS: readonly ["name_key", "country_id", "region_id", "placetype_id", "neg_rank", "spr_id", "name", "latitude", "longitude", "min_lat", "min_lon", "max_lat", "max_lon", "population", "is_primary"];
|
|
75
|
+
/**
|
|
76
|
+
* Create the code dictionaries + the transient staging table — called before the build's load
|
|
77
|
+
* passes. `cand_stage` mirrors {@link CandidateTable} but every column is nullable (the loader fills
|
|
78
|
+
* them positionally). Pass a {@link DatabaseClient} (or any `Kysely`) over the candidate DB.
|
|
79
|
+
*/
|
|
80
|
+
export declare function createCandidateStagingTables(db: Kysely<CandidateDatabase>): Promise<void>;
|
|
81
|
+
/**
|
|
82
|
+
* Create the clustered `WITHOUT ROWID` lookup table — called after staging, before the VACUUM. The
|
|
83
|
+
* first six columns form the clustered primary key (population-ranked via `neg_rank`).
|
|
84
|
+
*/
|
|
85
|
+
export declare function createCandidateTable(db: Kysely<CandidateDatabase>): Promise<void>;
|
|
86
|
+
//# sourceMappingURL=candidate-schema.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"candidate-schema.d.ts","sourceRoot":"","sources":["../candidate-schema.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,OAAO,EAAO,KAAK,MAAM,EAAE,MAAM,QAAQ,CAAA;AAEzC;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC9B,oFAAoF;IACpF,QAAQ,EAAE,MAAM,CAAA;IAChB,2EAA2E;IAC3E,UAAU,EAAE,MAAM,CAAA;IAClB,wFAAwF;IACxF,SAAS,EAAE,MAAM,CAAA;IACjB,iDAAiD;IACjD,YAAY,EAAE,MAAM,CAAA;IACpB;;;OAGG;IACH,QAAQ,EAAE,MAAM,CAAA;IAChB,gDAAgD;IAChD,MAAM,EAAE,MAAM,CAAA;IACd,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;IACnB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAA;IACvB,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAA;IACtB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;IACzB,yEAAyE;IACzE,UAAU,EAAE,MAAM,GAAG,IAAI,CAAA;CACzB;AAED,4CAA4C;AAC5C,MAAM,WAAW,gBAAgB;IAChC,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,qCAAqC;AACrC,MAAM,WAAW,kBAAkB;IAClC,EAAE,EAAE,MAAM,CAAA;IACV,SAAS,EAAE,MAAM,CAAA;CACjB;AAED,sFAAsF;AACtF,MAAM,WAAW,iBAAiB;IACjC,oEAAoE;IACpE,SAAS,EAAE,cAAc,CAAA;IACzB,wFAAwF;IACxF,UAAU,EAAE,cAAc,CAAA;IAC1B,aAAa,EAAE,gBAAgB,CAAA;IAC/B,eAAe,EAAE,kBAAkB,CAAA;CACnC;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,iMAgBpB,CAAA;AAEV;;;;GAIG;AACH,wBAAsB,4BAA4B,CAAC,EAAE,EAAE,MAAM,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6B/F;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CAAC,EAAE,EAAE,MAAM,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CA6BvF"}
|