@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.
Files changed (158) hide show
  1. package/README.md +250 -0
  2. package/out/address-point-interpolation.d.ts +48 -0
  3. package/out/address-point-interpolation.d.ts.map +1 -0
  4. package/out/address-point-interpolation.js +164 -0
  5. package/out/address-point-interpolation.js.map +1 -0
  6. package/out/address-point-schema.d.ts +58 -0
  7. package/out/address-point-schema.d.ts.map +1 -0
  8. package/out/address-point-schema.js +67 -0
  9. package/out/address-point-schema.js.map +1 -0
  10. package/out/address-point.d.ts +29 -0
  11. package/out/address-point.d.ts.map +1 -0
  12. package/out/address-point.js +62 -0
  13. package/out/address-point.js.map +1 -0
  14. package/out/ancestry.d.ts +40 -0
  15. package/out/ancestry.d.ts.map +1 -0
  16. package/out/ancestry.js +53 -0
  17. package/out/ancestry.js.map +1 -0
  18. package/out/build-candidate-cli.d.ts +16 -0
  19. package/out/build-candidate-cli.d.ts.map +1 -0
  20. package/out/build-candidate-cli.js +80 -0
  21. package/out/build-candidate-cli.js.map +1 -0
  22. package/out/build-candidate.d.ts +54 -0
  23. package/out/build-candidate.d.ts.map +1 -0
  24. package/out/build-candidate.js +230 -0
  25. package/out/build-candidate.js.map +1 -0
  26. package/out/build-coincident-roles-cli.d.ts +16 -0
  27. package/out/build-coincident-roles-cli.d.ts.map +1 -0
  28. package/out/build-coincident-roles-cli.js +94 -0
  29. package/out/build-coincident-roles-cli.js.map +1 -0
  30. package/out/build-fts-cli.d.ts +23 -0
  31. package/out/build-fts-cli.d.ts.map +1 -0
  32. package/out/build-fts-cli.js +117 -0
  33. package/out/build-fts-cli.js.map +1 -0
  34. package/out/build-slim-cli.d.ts +14 -0
  35. package/out/build-slim-cli.d.ts.map +1 -0
  36. package/out/build-slim-cli.js +130 -0
  37. package/out/build-slim-cli.js.map +1 -0
  38. package/out/build-slim.d.ts +71 -0
  39. package/out/build-slim.d.ts.map +1 -0
  40. package/out/build-slim.js +267 -0
  41. package/out/build-slim.js.map +1 -0
  42. package/out/candidate-lookup.d.ts +43 -0
  43. package/out/candidate-lookup.d.ts.map +1 -0
  44. package/out/candidate-lookup.js +191 -0
  45. package/out/candidate-lookup.js.map +1 -0
  46. package/out/candidate-schema.d.ts +86 -0
  47. package/out/candidate-schema.d.ts.map +1 -0
  48. package/out/candidate-schema.js +109 -0
  49. package/out/candidate-schema.js.map +1 -0
  50. package/out/coincident-roles.d.ts +86 -0
  51. package/out/coincident-roles.d.ts.map +1 -0
  52. package/out/coincident-roles.js +160 -0
  53. package/out/coincident-roles.js.map +1 -0
  54. package/out/convention.d.ts +109 -0
  55. package/out/convention.d.ts.map +1 -0
  56. package/out/convention.js +94 -0
  57. package/out/convention.js.map +1 -0
  58. package/out/fst-autocomplete.d.ts +49 -0
  59. package/out/fst-autocomplete.d.ts.map +1 -0
  60. package/out/fst-autocomplete.js +124 -0
  61. package/out/fst-autocomplete.js.map +1 -0
  62. package/out/fst-builder.d.ts +20 -0
  63. package/out/fst-builder.d.ts.map +1 -0
  64. package/out/fst-builder.js +219 -0
  65. package/out/fst-builder.js.map +1 -0
  66. package/out/fst-deserialize-web.d.ts +16 -0
  67. package/out/fst-deserialize-web.d.ts.map +1 -0
  68. package/out/fst-deserialize-web.js +133 -0
  69. package/out/fst-deserialize-web.js.map +1 -0
  70. package/out/fst-matcher.d.ts +33 -0
  71. package/out/fst-matcher.d.ts.map +1 -0
  72. package/out/fst-matcher.js +117 -0
  73. package/out/fst-matcher.js.map +1 -0
  74. package/out/fst-serialize.d.ts +30 -0
  75. package/out/fst-serialize.d.ts.map +1 -0
  76. package/out/fst-serialize.js +261 -0
  77. package/out/fst-serialize.js.map +1 -0
  78. package/out/fst-types.d.ts +60 -0
  79. package/out/fst-types.d.ts.map +1 -0
  80. package/out/fst-types.js +11 -0
  81. package/out/fst-types.js.map +1 -0
  82. package/out/fts.d.ts +158 -0
  83. package/out/fts.d.ts.map +1 -0
  84. package/out/fts.js +261 -0
  85. package/out/fts.js.map +1 -0
  86. package/out/geo.d.ts +74 -0
  87. package/out/geo.d.ts.map +1 -0
  88. package/out/geo.js +88 -0
  89. package/out/geo.js.map +1 -0
  90. package/out/index.d.ts +27 -0
  91. package/out/index.d.ts.map +1 -0
  92. package/out/index.js +22 -0
  93. package/out/index.js.map +1 -0
  94. package/out/interpolation.d.ts +84 -0
  95. package/out/interpolation.d.ts.map +1 -0
  96. package/out/interpolation.js +150 -0
  97. package/out/interpolation.js.map +1 -0
  98. package/out/lookup.d.ts +156 -0
  99. package/out/lookup.d.ts.map +1 -0
  100. package/out/lookup.js +876 -0
  101. package/out/lookup.js.map +1 -0
  102. package/out/postal-city-alias-lookup.d.ts +50 -0
  103. package/out/postal-city-alias-lookup.d.ts.map +1 -0
  104. package/out/postal-city-alias-lookup.js +66 -0
  105. package/out/postal-city-alias-lookup.js.map +1 -0
  106. package/out/postal-city-alias-schema.d.ts +51 -0
  107. package/out/postal-city-alias-schema.d.ts.map +1 -0
  108. package/out/postal-city-alias-schema.js +47 -0
  109. package/out/postal-city-alias-schema.js.map +1 -0
  110. package/out/postal-city-candidate-schema.d.ts +58 -0
  111. package/out/postal-city-candidate-schema.d.ts.map +1 -0
  112. package/out/postal-city-candidate-schema.js +56 -0
  113. package/out/postal-city-candidate-schema.js.map +1 -0
  114. package/out/postcode-point-lookup.d.ts +38 -0
  115. package/out/postcode-point-lookup.d.ts.map +1 -0
  116. package/out/postcode-point-lookup.js +46 -0
  117. package/out/postcode-point-lookup.js.map +1 -0
  118. package/out/reverse.d.ts +99 -0
  119. package/out/reverse.d.ts.map +1 -0
  120. package/out/reverse.js +290 -0
  121. package/out/reverse.js.map +1 -0
  122. package/out/schema.d.ts +163 -0
  123. package/out/schema.d.ts.map +1 -0
  124. package/out/schema.js +18 -0
  125. package/out/schema.js.map +1 -0
  126. package/out/sharding.d.ts +96 -0
  127. package/out/sharding.d.ts.map +1 -0
  128. package/out/sharding.js +129 -0
  129. package/out/sharding.js.map +1 -0
  130. package/out/sqlite-convention-source.d.ts +29 -0
  131. package/out/sqlite-convention-source.d.ts.map +1 -0
  132. package/out/sqlite-convention-source.js +53 -0
  133. package/out/sqlite-convention-source.js.map +1 -0
  134. package/out/sqlite-utils.d.ts +17 -0
  135. package/out/sqlite-utils.d.ts.map +1 -0
  136. package/out/sqlite-utils.js +24 -0
  137. package/out/sqlite-utils.js.map +1 -0
  138. package/out/street-morphology-fst-builder.d.ts +59 -0
  139. package/out/street-morphology-fst-builder.d.ts.map +1 -0
  140. package/out/street-morphology-fst-builder.js +174 -0
  141. package/out/street-morphology-fst-builder.js.map +1 -0
  142. package/out/street-normalize.d.ts +66 -0
  143. package/out/street-normalize.d.ts.map +1 -0
  144. package/out/street-normalize.js +176 -0
  145. package/out/street-normalize.js.map +1 -0
  146. package/out/street-segment-schema.d.ts +61 -0
  147. package/out/street-segment-schema.d.ts.map +1 -0
  148. package/out/street-segment-schema.js +64 -0
  149. package/out/street-segment-schema.js.map +1 -0
  150. package/out/types.d.ts +137 -0
  151. package/out/types.d.ts.map +1 -0
  152. package/out/types.js +13 -0
  153. package/out/types.js.map +1 -0
  154. package/out/unified-schema.d.ts +25 -0
  155. package/out/unified-schema.d.ts.map +1 -0
  156. package/out/unified-schema.js +142 -0
  157. package/out/unified-schema.js.map +1 -0
  158. 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":"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"}
@@ -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"}