@mailwoman/resolver-wof-sqlite 7.2.0 → 7.3.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 (57) hide show
  1. package/address-point-interpolation.ts +207 -0
  2. package/address-point-schema.ts +107 -0
  3. package/address-point.ts +122 -0
  4. package/ancestry-backfill.ts +205 -0
  5. package/ancestry.ts +70 -0
  6. package/build-candidate.ts +351 -0
  7. package/build-slim.ts +394 -0
  8. package/candidate-fts.ts +43 -0
  9. package/candidate-lookup.ts +382 -0
  10. package/candidate-schema.ts +166 -0
  11. package/coincident-roles.ts +240 -0
  12. package/convention.ts +152 -0
  13. package/fst-autocomplete.ts +187 -0
  14. package/fst-builder.ts +291 -0
  15. package/fst-deserialize-web.ts +164 -0
  16. package/fst-matcher.ts +150 -0
  17. package/fst-serialize.ts +311 -0
  18. package/fst-types.ts +78 -0
  19. package/fts.ts +318 -0
  20. package/geo.ts +140 -0
  21. package/geonames-aliases.ts +317 -0
  22. package/geonames-postal.ts +150 -0
  23. package/index.ts +117 -0
  24. package/interpolation.ts +232 -0
  25. package/lookup.ts +1498 -0
  26. package/out/poi-lookup.d.ts +14 -2
  27. package/out/poi-lookup.d.ts.map +1 -1
  28. package/out/poi-lookup.js +55 -21
  29. package/out/poi-lookup.js.map +1 -1
  30. package/out/poi-schema.d.ts +9 -0
  31. package/out/poi-schema.d.ts.map +1 -1
  32. package/out/poi-schema.js +16 -0
  33. package/out/poi-schema.js.map +1 -1
  34. package/out/reverse.d.ts +8 -1
  35. package/out/reverse.d.ts.map +1 -1
  36. package/out/reverse.js +10 -1
  37. package/out/reverse.js.map +1 -1
  38. package/package.json +168 -82
  39. package/poi-lookup.ts +375 -0
  40. package/poi-schema.ts +164 -0
  41. package/postal-city-alias-lookup.ts +89 -0
  42. package/postal-city-alias-schema.ts +75 -0
  43. package/postal-city-candidate-schema.ts +81 -0
  44. package/postcode-point-lookup.ts +64 -0
  45. package/reverse.ts +439 -0
  46. package/schema.ts +176 -0
  47. package/sharding.ts +235 -0
  48. package/sqlite-convention-source.ts +61 -0
  49. package/sqlite-utils.ts +25 -0
  50. package/street-centroid-schema.ts +124 -0
  51. package/street-centroid.ts +124 -0
  52. package/street-morphology-fst-builder.ts +230 -0
  53. package/street-name-lookup.ts +101 -0
  54. package/street-normalize.ts +302 -0
  55. package/street-segment-schema.ts +104 -0
  56. package/types.ts +164 -0
  57. package/unified-schema.ts +171 -0
@@ -0,0 +1,171 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Schema for the unified WOF SQLite database we build from cloned WOF GeoJSON repos
7
+ * (`scripts/build-unified-wof.ts`). This is the CANONICAL gazetteer — we never use the
8
+ * off-the-shelf geocode.earth prebuilt dumps (they assign different WOF ids to the same place;
9
+ * see the `feedback-custom-wof-db-only` memory). The table/column names match the resolver's
10
+ * expectations (`lookup.ts`) so `WOFSqlitePlaceLookup` works unchanged, INCLUDING the `ancestors`
11
+ * table (which lookup.ts's parent-constraint subquery needs) — see `populateAncestors`. The
12
+ * `place_search` FTS5 + `place_bbox` R*Tree are built separately by `build-fts` (fts.ts).
13
+ */
14
+
15
+ import { DatabaseSync } from "node:sqlite"
16
+
17
+ import { DatabaseClient } from "@mailwoman/core/kysley/client"
18
+
19
+ import type { WOFDatabase } from "./schema.ts"
20
+
21
+ export async function createUnifiedSchema(db: DatabaseSync): Promise<void> {
22
+ // PRAGMAs stay raw — not Kysely-modelled, and these tune the bulk build.
23
+ db.exec("PRAGMA journal_mode = WAL")
24
+ db.exec("PRAGMA busy_timeout = 10000")
25
+ db.exec("PRAGMA synchronous = OFF")
26
+
27
+ // `kdb` wraps `db` for the DDL (the house idiom); the caller owns `db`'s lifecycle, so we don't
28
+ // destroy it here. The bulk INSERTs (populateAncestors + build-unified-wof) stay on the raw handle.
29
+ const kdb = new DatabaseClient<WOFDatabase>({ database: db })
30
+
31
+ await kdb.schema
32
+ .createTable("spr")
33
+ .ifNotExists()
34
+ .addColumn("id", "integer", (c) => c.primaryKey())
35
+ .addColumn("parent_id", "integer", (c) => c.notNull().defaultTo(-1))
36
+ .addColumn("name", "text", (c) => c.notNull().defaultTo(""))
37
+ .addColumn("placetype", "text", (c) => c.notNull().defaultTo(""))
38
+ .addColumn("country", "text", (c) => c.notNull().defaultTo(""))
39
+ .addColumn("latitude", "real", (c) => c.notNull().defaultTo(0))
40
+ .addColumn("longitude", "real", (c) => c.notNull().defaultTo(0))
41
+ .addColumn("min_latitude", "real", (c) => c.notNull().defaultTo(0))
42
+ .addColumn("min_longitude", "real", (c) => c.notNull().defaultTo(0))
43
+ .addColumn("max_latitude", "real", (c) => c.notNull().defaultTo(0))
44
+ .addColumn("max_longitude", "real", (c) => c.notNull().defaultTo(0))
45
+ .addColumn("is_current", "integer", (c) => c.notNull().defaultTo(1))
46
+ .addColumn("is_deprecated", "integer", (c) => c.notNull().defaultTo(0))
47
+ .addColumn("is_ceased", "integer", (c) => c.notNull().defaultTo(0))
48
+ .addColumn("is_superseded", "integer", (c) => c.notNull().defaultTo(0))
49
+ .addColumn("is_superseding", "integer", (c) => c.notNull().defaultTo(0))
50
+ .addColumn("lastmodified", "integer", (c) => c.notNull().defaultTo(0))
51
+ .execute()
52
+
53
+ // `privateuse` carries WOF's x_<variant> kind (preferred | variant) / GeoNames' isPreferredName
54
+ // ("preferred" | ""). `official` is the #936 ingest bit: 1 when the row's language is an official
55
+ // language of the place's country (codex OFFICIAL_LANGUAGES) AND the row is a preferred form —
56
+ // x_variant rows tagged with an official language ("MSP", "Frisco") stay 0. Primary-name mirror
57
+ // rows stay 0 too: the name-exact tier already consults spr.name; `official` only marks the
58
+ // ALIASES eligible to join it. Both are ingest-time facts, never computed at query time.
59
+ await kdb.schema
60
+ .createTable("names")
61
+ .ifNotExists()
62
+ .addColumn("id", "integer", (c) => c.notNull())
63
+ .addColumn("name", "text", (c) => c.notNull())
64
+ .addColumn("placetype", "text", (c) => c.notNull().defaultTo(""))
65
+ .addColumn("country", "text", (c) => c.notNull().defaultTo(""))
66
+ .addColumn("language", "text", (c) => c.notNull().defaultTo(""))
67
+ .addColumn("privateuse", "text", (c) => c.notNull().defaultTo(""))
68
+ .addColumn("official", "integer", (c) => c.notNull().defaultTo(0))
69
+ .addColumn("lastmodified", "integer", (c) => c.notNull().defaultTo(0))
70
+ .execute()
71
+
72
+ await kdb.schema
73
+ .createTable("concordances")
74
+ .ifNotExists()
75
+ .addColumn("id", "integer", (c) => c.notNull())
76
+ .addColumn("other_id", "text", (c) => c.notNull())
77
+ .addColumn("other_source", "text", (c) => c.notNull())
78
+ .addColumn("lastmodified", "integer", (c) => c.notNull().defaultTo(0))
79
+ .execute()
80
+
81
+ await kdb.schema
82
+ .createTable("place_population")
83
+ .ifNotExists()
84
+ .addColumn("id", "integer", (c) => c.primaryKey())
85
+ .addColumn("population", "integer", (c) => c.notNull().defaultTo(0))
86
+ .execute()
87
+
88
+ // `ancestors` maps each place to every place above it in the hierarchy (and itself). The
89
+ // resolver's parent-constraint scopes a child lookup to a parent's descendants via
90
+ // `spr.id IN (SELECT id FROM ancestors WHERE ancestor_id = ?)`. The off-the-shelf WOF dumps
91
+ // ship this table; our build derives it from the parent_id chain (see populateAncestors) since
92
+ // we don't capture `wof:hierarchy`.
93
+ await kdb.schema
94
+ .createTable("ancestors")
95
+ .ifNotExists()
96
+ .addColumn("id", "integer", (c) => c.notNull())
97
+ .addColumn("ancestor_id", "integer", (c) => c.notNull())
98
+ .addColumn("ancestor_placetype", "text", (c) => c.notNull().defaultTo(""))
99
+ .addColumn("lastmodified", "integer", (c) => c.notNull().defaultTo(0))
100
+ .execute()
101
+ }
102
+
103
+ /**
104
+ * Populate the `ancestors` table by walking each place's `parent_id` chain in `spr` (transitive closure, including the
105
+ * place itself). Idempotent: drops + rebuilds the table contents. Returns the row count. Run after `spr` is fully
106
+ * ingested (build-unified-wof freeze phase) or standalone on an existing unified DB (`scripts/add-ancestors.ts`).
107
+ * Sentinel/negative parent_ids and cycles terminate the walk. ~4 rows/place average; a transaction keeps the ~5M
108
+ * inserts fast.
109
+ */
110
+ export function populateAncestors(db: DatabaseSync): number {
111
+ db.exec("DELETE FROM ancestors")
112
+ const rows = db.prepare("SELECT id, parent_id, placetype FROM spr").all() as Array<{
113
+ id: number
114
+ parent_id: number
115
+ placetype: string
116
+ }>
117
+ const byID = new Map<number, { parent: number; placetype: string }>()
118
+
119
+ for (const r of rows) {
120
+ byID.set(r.id, { parent: r.parent_id, placetype: r.placetype })
121
+ }
122
+
123
+ const insert = db.prepare("INSERT INTO ancestors (id, ancestor_id, ancestor_placetype) VALUES (?, ?, ?)")
124
+ db.exec("BEGIN")
125
+ let count = 0
126
+
127
+ for (const r of rows) {
128
+ insert.run(r.id, r.id, r.placetype) // self
129
+ count++
130
+ const seen = new Set<number>([r.id])
131
+ let cur = r.parent_id
132
+
133
+ while (cur > 0 && !seen.has(cur)) {
134
+ const node = byID.get(cur)
135
+
136
+ if (!node) break
137
+ insert.run(r.id, cur, node.placetype)
138
+ count++
139
+ seen.add(cur)
140
+ cur = node.parent
141
+ }
142
+ }
143
+ db.exec("COMMIT")
144
+
145
+ return count
146
+ }
147
+
148
+ export async function createUnifiedIndexes(db: DatabaseSync): Promise<void> {
149
+ const kdb = new DatabaseClient<WOFDatabase>({ database: db })
150
+ await kdb.schema.createIndex("spr_by_placetype").ifNotExists().on("spr").column("placetype").execute()
151
+ await kdb.schema.createIndex("spr_by_country").ifNotExists().on("spr").column("country").execute()
152
+ await kdb.schema.createIndex("spr_by_parent").ifNotExists().on("spr").column("parent_id").execute()
153
+ await kdb.schema.createIndex("names_by_id").ifNotExists().on("names").column("id").execute()
154
+ await kdb.schema.createIndex("names_by_name").ifNotExists().on("names").column("name").execute()
155
+ await kdb.schema
156
+ .createIndex("concordances_by_id")
157
+ .ifNotExists()
158
+ .on("concordances")
159
+ .columns(["id", "lastmodified"])
160
+ .execute()
161
+ await kdb.schema
162
+ .createIndex("concordances_by_other_id")
163
+ .ifNotExists()
164
+ .on("concordances")
165
+ .columns(["other_source", "other_id"])
166
+ .execute()
167
+ // ancestor_id is the hot column (parent-constraint queries `WHERE ancestor_id = ?`); id supports
168
+ // the reverse lookup.
169
+ await kdb.schema.createIndex("ancestors_by_ancestor").ifNotExists().on("ancestors").column("ancestor_id").execute()
170
+ await kdb.schema.createIndex("ancestors_by_id").ifNotExists().on("ancestors").column("id").execute()
171
+ }