@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.
- package/address-point-interpolation.ts +207 -0
- package/address-point-schema.ts +107 -0
- package/address-point.ts +122 -0
- package/ancestry-backfill.ts +205 -0
- package/ancestry.ts +70 -0
- package/build-candidate.ts +351 -0
- package/build-slim.ts +394 -0
- package/candidate-fts.ts +43 -0
- package/candidate-lookup.ts +382 -0
- package/candidate-schema.ts +166 -0
- package/coincident-roles.ts +240 -0
- package/convention.ts +152 -0
- package/fst-autocomplete.ts +187 -0
- package/fst-builder.ts +291 -0
- package/fst-deserialize-web.ts +164 -0
- package/fst-matcher.ts +150 -0
- package/fst-serialize.ts +311 -0
- package/fst-types.ts +78 -0
- package/fts.ts +318 -0
- package/geo.ts +140 -0
- package/geonames-aliases.ts +317 -0
- package/geonames-postal.ts +150 -0
- package/index.ts +117 -0
- package/interpolation.ts +232 -0
- package/lookup.ts +1498 -0
- package/out/poi-lookup.d.ts +14 -2
- package/out/poi-lookup.d.ts.map +1 -1
- package/out/poi-lookup.js +55 -21
- package/out/poi-lookup.js.map +1 -1
- package/out/poi-schema.d.ts +9 -0
- package/out/poi-schema.d.ts.map +1 -1
- package/out/poi-schema.js +16 -0
- package/out/poi-schema.js.map +1 -1
- package/out/reverse.d.ts +8 -1
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +10 -1
- package/out/reverse.js.map +1 -1
- package/package.json +168 -82
- package/poi-lookup.ts +375 -0
- package/poi-schema.ts +164 -0
- package/postal-city-alias-lookup.ts +89 -0
- package/postal-city-alias-schema.ts +75 -0
- package/postal-city-candidate-schema.ts +81 -0
- package/postcode-point-lookup.ts +64 -0
- package/reverse.ts +439 -0
- package/schema.ts +176 -0
- package/sharding.ts +235 -0
- package/sqlite-convention-source.ts +61 -0
- package/sqlite-utils.ts +25 -0
- package/street-centroid-schema.ts +124 -0
- package/street-centroid.ts +124 -0
- package/street-morphology-fst-builder.ts +230 -0
- package/street-name-lookup.ts +101 -0
- package/street-normalize.ts +302 -0
- package/street-segment-schema.ts +104 -0
- package/types.ts +164 -0
- 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
|
+
}
|