@mailwoman/resolver-wof-sqlite 8.1.0 → 8.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 +9 -3
- package/address-point-schema.ts +32 -10
- package/address-point.ts +3 -0
- package/ancestry-backfill.ts +18 -5
- package/ancestry.ts +7 -2
- package/build-candidate.ts +29 -6
- package/build-slim.ts +40 -11
- package/candidate-fts.ts +1 -0
- package/candidate-lookup.ts +36 -14
- package/candidate-schema.ts +35 -11
- package/coincident-roles.ts +28 -6
- package/convention.ts +3 -1
- package/coverage-manifest-schema.ts +49 -16
- package/fst-autocomplete.ts +15 -9
- package/fst-builder.ts +29 -5
- package/fst-deserialize-web.ts +46 -9
- package/fst-matcher.ts +12 -5
- package/fst-serialize.ts +83 -9
- package/fst-types.ts +26 -3
- package/fts-query.ts +84 -0
- package/fts.ts +35 -9
- package/geo.ts +9 -3
- package/geonames-aliases.ts +116 -79
- package/geonames-postal.ts +25 -5
- package/index.ts +10 -0
- package/interpolation.ts +33 -55
- package/lookup.ts +103 -292
- package/name-score.ts +76 -0
- package/out/address-point-interpolation.d.ts.map +1 -1
- package/out/address-point-interpolation.js +4 -2
- package/out/address-point-interpolation.js.map +1 -1
- package/out/address-point-schema.d.ts +30 -10
- package/out/address-point-schema.d.ts.map +1 -1
- package/out/address-point-schema.js +6 -2
- package/out/address-point-schema.js.map +1 -1
- package/out/address-point.d.ts.map +1 -1
- package/out/address-point.js.map +1 -1
- package/out/ancestry-backfill.d.ts +6 -2
- package/out/ancestry-backfill.d.ts.map +1 -1
- package/out/ancestry-backfill.js +7 -3
- package/out/ancestry-backfill.js.map +1 -1
- package/out/ancestry.d.ts +6 -2
- package/out/ancestry.d.ts.map +1 -1
- package/out/ancestry.js +3 -1
- package/out/ancestry.js.map +1 -1
- package/out/build-candidate.d.ts +9 -3
- package/out/build-candidate.d.ts.map +1 -1
- package/out/build-candidate.js +5 -3
- package/out/build-candidate.js.map +1 -1
- package/out/build-slim.d.ts +15 -5
- package/out/build-slim.d.ts.map +1 -1
- package/out/build-slim.js +11 -5
- package/out/build-slim.js.map +1 -1
- package/out/candidate-fts.d.ts.map +1 -1
- package/out/candidate-fts.js.map +1 -1
- package/out/candidate-lookup.d.ts +9 -3
- package/out/candidate-lookup.d.ts.map +1 -1
- package/out/candidate-lookup.js +16 -11
- package/out/candidate-lookup.js.map +1 -1
- package/out/candidate-schema.d.ts +33 -11
- package/out/candidate-schema.d.ts.map +1 -1
- package/out/candidate-schema.js.map +1 -1
- package/out/coincident-roles.d.ts +16 -4
- package/out/coincident-roles.d.ts.map +1 -1
- package/out/coincident-roles.js +9 -3
- package/out/coincident-roles.js.map +1 -1
- package/out/convention.d.ts +3 -1
- package/out/convention.d.ts.map +1 -1
- package/out/convention.js.map +1 -1
- package/out/coverage-manifest-schema.d.ts +45 -14
- package/out/coverage-manifest-schema.d.ts.map +1 -1
- package/out/coverage-manifest-schema.js +14 -5
- package/out/coverage-manifest-schema.js.map +1 -1
- package/out/fst-autocomplete.d.ts +1 -1
- package/out/fst-autocomplete.d.ts.map +1 -1
- package/out/fst-autocomplete.js +11 -9
- package/out/fst-autocomplete.js.map +1 -1
- package/out/fst-builder.d.ts.map +1 -1
- package/out/fst-builder.js +15 -5
- package/out/fst-builder.js.map +1 -1
- package/out/fst-deserialize-web.d.ts.map +1 -1
- package/out/fst-deserialize-web.js +34 -9
- package/out/fst-deserialize-web.js.map +1 -1
- package/out/fst-matcher.d.ts +6 -2
- package/out/fst-matcher.d.ts.map +1 -1
- package/out/fst-matcher.js +9 -5
- package/out/fst-matcher.js.map +1 -1
- package/out/fst-serialize.d.ts.map +1 -1
- package/out/fst-serialize.js +62 -9
- package/out/fst-serialize.js.map +1 -1
- package/out/fst-types.d.ts +26 -3
- package/out/fst-types.d.ts.map +1 -1
- package/out/fts-query.d.ts +41 -0
- package/out/fts-query.d.ts.map +1 -0
- package/out/fts-query.js +75 -0
- package/out/fts-query.js.map +1 -0
- package/out/fts.d.ts +21 -7
- package/out/fts.d.ts.map +1 -1
- package/out/fts.js +10 -4
- package/out/fts.js.map +1 -1
- package/out/geo.d.ts +6 -2
- package/out/geo.d.ts.map +1 -1
- package/out/geo.js +3 -1
- package/out/geo.js.map +1 -1
- package/out/geonames-aliases.d.ts +12 -4
- package/out/geonames-aliases.d.ts.map +1 -1
- package/out/geonames-aliases.js +72 -67
- package/out/geonames-aliases.js.map +1 -1
- package/out/geonames-postal.d.ts +9 -3
- package/out/geonames-postal.d.ts.map +1 -1
- package/out/geonames-postal.js +7 -2
- package/out/geonames-postal.js.map +1 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js.map +1 -1
- package/out/interpolation.d.ts +18 -6
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +11 -40
- package/out/interpolation.js.map +1 -1
- package/out/lookup.d.ts +3 -97
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +52 -184
- package/out/lookup.js.map +1 -1
- package/out/name-score.d.ts +28 -0
- package/out/name-score.d.ts.map +1 -0
- package/out/name-score.js +67 -0
- package/out/name-score.js.map +1 -0
- package/out/poi-lookup.d.ts +24 -8
- package/out/poi-lookup.d.ts.map +1 -1
- package/out/poi-lookup.js +27 -13
- package/out/poi-lookup.js.map +1 -1
- package/out/poi-schema.d.ts +42 -13
- package/out/poi-schema.d.ts.map +1 -1
- package/out/poi-schema.js +12 -3
- package/out/poi-schema.js.map +1 -1
- package/out/postal-city-alias-lookup.d.ts +18 -6
- package/out/postal-city-alias-lookup.d.ts.map +1 -1
- package/out/postal-city-alias-lookup.js.map +1 -1
- package/out/postal-city-alias-schema.d.ts +27 -9
- package/out/postal-city-alias-schema.d.ts.map +1 -1
- package/out/postal-city-alias-schema.js +3 -1
- package/out/postal-city-alias-schema.js.map +1 -1
- package/out/postal-city-candidate-schema.d.ts +15 -5
- package/out/postal-city-candidate-schema.d.ts.map +1 -1
- package/out/postal-city-candidate-schema.js +3 -1
- package/out/postal-city-candidate-schema.js.map +1 -1
- package/out/postcode-point-lookup.d.ts +6 -2
- package/out/postcode-point-lookup.d.ts.map +1 -1
- package/out/postcode-point-lookup.js +6 -2
- package/out/postcode-point-lookup.js.map +1 -1
- package/out/ranking-weights.d.ts +118 -0
- package/out/ranking-weights.d.ts.map +1 -0
- package/out/ranking-weights.js +44 -0
- package/out/ranking-weights.js.map +1 -0
- package/out/reverse.d.ts +9 -3
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +20 -6
- package/out/reverse.js.map +1 -1
- package/out/sharding.d.ts +3 -1
- package/out/sharding.d.ts.map +1 -1
- package/out/sharding.js +7 -5
- package/out/sharding.js.map +1 -1
- package/out/sqlite-convention-source.d.ts.map +1 -1
- package/out/sqlite-convention-source.js +3 -1
- package/out/sqlite-convention-source.js.map +1 -1
- package/out/street-centroid-schema.d.ts +33 -11
- package/out/street-centroid-schema.d.ts.map +1 -1
- package/out/street-centroid-schema.js +3 -1
- package/out/street-centroid-schema.js.map +1 -1
- package/out/street-centroid.d.ts.map +1 -1
- package/out/street-centroid.js +6 -2
- package/out/street-centroid.js.map +1 -1
- package/out/street-morphology-fst-builder.d.ts +6 -2
- package/out/street-morphology-fst-builder.d.ts.map +1 -1
- package/out/street-morphology-fst-builder.js +8 -7
- package/out/street-morphology-fst-builder.js.map +1 -1
- package/out/street-morphology-fst-loader.d.ts +24 -8
- package/out/street-morphology-fst-loader.d.ts.map +1 -1
- package/out/street-morphology-fst-loader.js +11 -5
- package/out/street-morphology-fst-loader.js.map +1 -1
- package/out/street-name-lookup.d.ts +9 -3
- package/out/street-name-lookup.d.ts.map +1 -1
- package/out/street-name-lookup.js +9 -7
- package/out/street-name-lookup.js.map +1 -1
- package/out/street-normalize.d.ts +3 -1
- package/out/street-normalize.d.ts.map +1 -1
- package/out/street-normalize.js +23 -13
- package/out/street-normalize.js.map +1 -1
- package/out/street-segment-schema.d.ts +48 -16
- package/out/street-segment-schema.d.ts.map +1 -1
- package/out/street-segment-schema.js +6 -2
- package/out/street-segment-schema.js.map +1 -1
- package/out/types.d.ts +18 -6
- package/out/types.d.ts.map +1 -1
- package/out/unified-schema.d.ts +1 -1
- package/out/unified-schema.d.ts.map +1 -1
- package/out/unified-schema.js +2 -2
- package/out/unified-schema.js.map +1 -1
- package/package.json +5 -5
- package/poi-lookup.ts +53 -21
- package/poi-schema.ts +43 -13
- package/postal-city-alias-lookup.ts +20 -6
- package/postal-city-alias-schema.ts +28 -9
- package/postal-city-candidate-schema.ts +15 -5
- package/postcode-point-lookup.ts +6 -2
- package/ranking-weights.ts +148 -0
- package/reverse.ts +47 -10
- package/sharding.ts +13 -6
- package/sqlite-convention-source.ts +4 -1
- package/street-centroid-schema.ts +35 -11
- package/street-centroid.ts +10 -3
- package/street-morphology-fst-builder.ts +25 -9
- package/street-morphology-fst-loader.ts +26 -10
- package/street-name-lookup.ts +19 -7
- package/street-normalize.ts +28 -13
- package/street-segment-schema.ts +50 -16
- package/types.ts +18 -6
- package/unified-schema.ts +11 -2
|
@@ -40,7 +40,9 @@ const STREET_AFFIX_WOFID_BASE = 1_900_000_000
|
|
|
40
40
|
const STREET_TYPES_FILENAME = "street_types.txt"
|
|
41
41
|
|
|
42
42
|
export interface BuildStreetMorphologyFSTOpts {
|
|
43
|
-
/**
|
|
43
|
+
/**
|
|
44
|
+
* Path to the `core/data/libpostal/dictionaries` directory containing per-locale subfolders.
|
|
45
|
+
*/
|
|
44
46
|
dictionariesDir: string
|
|
45
47
|
/**
|
|
46
48
|
* Optional locale filter — only ingest these locale subfolders. Defaults to all that have a `street_types.txt`.
|
|
@@ -56,7 +58,9 @@ export interface BuildStreetMorphologyFSTOpts {
|
|
|
56
58
|
* `rue`, `str`) while filtering out the noise.
|
|
57
59
|
*/
|
|
58
60
|
minVariantLength?: number
|
|
59
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Optional progress callback.
|
|
63
|
+
*/
|
|
60
64
|
onProgress?: (phase: string, detail?: string) => void
|
|
61
65
|
}
|
|
62
66
|
|
|
@@ -78,13 +82,14 @@ export interface BuildStreetMorphologyFSTResult {
|
|
|
78
82
|
function parseLine(line: string): { canonical: string; variants: string[] } | null {
|
|
79
83
|
const trimmed = line.trim()
|
|
80
84
|
|
|
81
|
-
if (trimmed.length
|
|
85
|
+
if (!trimmed.length || trimmed.startsWith("#")) return null
|
|
86
|
+
|
|
82
87
|
const parts = trimmed
|
|
83
88
|
.split("|")
|
|
84
89
|
.map((s) => s.trim())
|
|
85
90
|
.filter((s) => s.length > 0)
|
|
86
91
|
|
|
87
|
-
if (parts.length
|
|
92
|
+
if (!parts.length) return null
|
|
88
93
|
|
|
89
94
|
return { canonical: parts[0]!, variants: parts }
|
|
90
95
|
}
|
|
@@ -96,7 +101,8 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
96
101
|
// Discover locales — either provided explicitly, or all directories containing street_types.txt.
|
|
97
102
|
let locales: string[]
|
|
98
103
|
|
|
99
|
-
|
|
104
|
+
// oxlint-disable-next-line unicorn/prefer-ternary -- the else branch is a multi-line directory scan
|
|
105
|
+
if (opts.locales && opts.locales.length) {
|
|
100
106
|
locales = opts.locales
|
|
101
107
|
} else {
|
|
102
108
|
locales = readdirSync(opts.dictionariesDir).filter((entry) => {
|
|
@@ -113,6 +119,7 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
113
119
|
}
|
|
114
120
|
})
|
|
115
121
|
}
|
|
122
|
+
|
|
116
123
|
progress("discover", `Found ${locales.length} locales with ${STREET_TYPES_FILENAME}`)
|
|
117
124
|
|
|
118
125
|
// Collect canonical → set-of-variants across all locales. Same canonical form may appear in
|
|
@@ -132,13 +139,15 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
132
139
|
for (const variant of parsed.variants) {
|
|
133
140
|
existing.add(variant)
|
|
134
141
|
}
|
|
142
|
+
|
|
135
143
|
canonicalToVariants.set(parsed.canonical, existing)
|
|
136
144
|
}
|
|
137
145
|
}
|
|
146
|
+
|
|
138
147
|
progress("collect", `Collected ${canonicalToVariants.size} canonical affixes`)
|
|
139
148
|
|
|
140
149
|
// Assign stable synthetic wofIDs. Sort canonicals for determinism.
|
|
141
|
-
const sortedCanonicals = [...canonicalToVariants.keys()].
|
|
150
|
+
const sortedCanonicals = [...canonicalToVariants.keys()].toSorted()
|
|
142
151
|
const canonicalToWOFID = new Map<string, number>()
|
|
143
152
|
|
|
144
153
|
for (let i = 0; i < sortedCanonicals.length; i++) {
|
|
@@ -150,7 +159,7 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
150
159
|
const nodes: FSTNode[] = [{ edges: new Map(), places: [] }]
|
|
151
160
|
|
|
152
161
|
function insertName(tokens: string[], entry: PlaceEntry): void {
|
|
153
|
-
if (tokens.length
|
|
162
|
+
if (!tokens.length) return
|
|
154
163
|
let stateID = 0
|
|
155
164
|
|
|
156
165
|
for (const t of tokens) {
|
|
@@ -162,8 +171,10 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
162
171
|
nodes.push({ edges: new Map(), places: [] })
|
|
163
172
|
node.edges.set(t, next)
|
|
164
173
|
}
|
|
174
|
+
|
|
165
175
|
stateID = next
|
|
166
176
|
}
|
|
177
|
+
|
|
167
178
|
const existing = nodes[stateID]!.places
|
|
168
179
|
|
|
169
180
|
if (!existing.some((p) => p.wofID === entry.wofID && p.placetype === entry.placetype)) {
|
|
@@ -177,6 +188,7 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
177
188
|
for (const canonical of sortedCanonicals) {
|
|
178
189
|
const variants = canonicalToVariants.get(canonical)!
|
|
179
190
|
const wofID = canonicalToWOFID.get(canonical)!
|
|
191
|
+
|
|
180
192
|
const entry: PlaceEntry = {
|
|
181
193
|
wofID,
|
|
182
194
|
placetype: "street_affix",
|
|
@@ -185,7 +197,7 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
185
197
|
// Fixed importance: street affixes are structurally unambiguous (Avenue is almost never
|
|
186
198
|
// anything but street-typing). The morphology prior caps bias separately; this value
|
|
187
199
|
// just feeds the cap formula `importance * cap`.
|
|
188
|
-
importance: 1
|
|
200
|
+
importance: 1,
|
|
189
201
|
lat: 0,
|
|
190
202
|
lon: 0,
|
|
191
203
|
}
|
|
@@ -193,21 +205,25 @@ export function buildStreetMorphologyFST(opts: BuildStreetMorphologyFSTOpts): Bu
|
|
|
193
205
|
for (const variant of variants) {
|
|
194
206
|
const tokens = normalizeTokens(variant)
|
|
195
207
|
|
|
196
|
-
if (tokens.length
|
|
208
|
+
if (!tokens.length) continue
|
|
197
209
|
// Filter out collision-prone short surface forms — see `minVariantLength` docstring.
|
|
198
210
|
// We measure against the joined token form (no spaces) since FST keys are token sequences.
|
|
199
211
|
const joined = tokens.join("")
|
|
200
212
|
|
|
201
213
|
if (joined.length < minVariantLength) continue
|
|
202
214
|
insertName(tokens, entry)
|
|
215
|
+
|
|
203
216
|
insertCount++
|
|
217
|
+
|
|
204
218
|
variantCount++
|
|
205
219
|
}
|
|
206
220
|
}
|
|
221
|
+
|
|
207
222
|
progress("trie", `Built trie: ${nodes.length} states, ${insertCount} variant insertions`)
|
|
208
223
|
|
|
209
224
|
const edgeCount = nodes.reduce((sum, n) => sum + n.edges.size, 0)
|
|
210
225
|
const matcher = FSTMatcher.fromNodes(nodes)
|
|
226
|
+
|
|
211
227
|
const provenance: FSTProvenance = {
|
|
212
228
|
builtAt: new Date().toISOString(),
|
|
213
229
|
countries: locales, // Reuse `countries` slot for locale provenance — semantics differ from admin FST.
|
|
@@ -30,10 +30,14 @@ import { deserializeFST, readFSTProvenance } from "./fst-serialize.ts"
|
|
|
30
30
|
import type { FSTProvenance } from "./fst-types.ts"
|
|
31
31
|
import { buildStreetMorphologyFST } from "./street-morphology-fst-builder.ts"
|
|
32
32
|
|
|
33
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* The sealed artifact's canonical filename — identical in the data-root staging dir and as a weights-package sibling.
|
|
35
|
+
*/
|
|
34
36
|
export const STREET_MORPHOLOGY_ARTIFACT_FILENAME = "fst-street-morphology.bin"
|
|
35
37
|
|
|
36
|
-
/**
|
|
38
|
+
/**
|
|
39
|
+
* The staged artifact's default location: `$MAILWOMAN_DATA_ROOT/wof/fst-street-morphology.bin`.
|
|
40
|
+
*/
|
|
37
41
|
export function defaultStreetMorphologyArtifactPath(): string {
|
|
38
42
|
return String(dataRootPath("wof", STREET_MORPHOLOGY_ARTIFACT_FILENAME))
|
|
39
43
|
}
|
|
@@ -44,23 +48,35 @@ export interface LoadStreetMorphologyFSTOpts {
|
|
|
44
48
|
* unreadable degrades straight to the dictionary build, never a throw.
|
|
45
49
|
*/
|
|
46
50
|
artifactPath?: string
|
|
47
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Dictionaries dir for the build fallback. Defaults to core's bundled libpostal dictionaries.
|
|
53
|
+
*/
|
|
48
54
|
dictionariesDir?: string
|
|
49
|
-
/**
|
|
55
|
+
/**
|
|
56
|
+
* Unreadable-artifact diagnostics. Defaults to silent (the caller owns its warn channel).
|
|
57
|
+
*/
|
|
50
58
|
onWarn?: (message: string) => void
|
|
51
59
|
}
|
|
52
60
|
|
|
53
61
|
export interface LoadedStreetMorphologyFST {
|
|
54
62
|
matcher: FSTMatcher
|
|
55
|
-
/**
|
|
63
|
+
/**
|
|
64
|
+
* Which rung produced the matcher: a sealed-artifact deserialize, or the per-process dictionary build.
|
|
65
|
+
*/
|
|
56
66
|
source: "artifact" | "built"
|
|
57
|
-
/**
|
|
67
|
+
/**
|
|
68
|
+
* The artifact path when `source === "artifact"`.
|
|
69
|
+
*/
|
|
58
70
|
path?: string
|
|
59
|
-
/**
|
|
71
|
+
/**
|
|
72
|
+
* Build provenance — read from the artifact trailer, or carried fresh off the fallback build.
|
|
73
|
+
*/
|
|
60
74
|
provenance?: FSTProvenance
|
|
61
75
|
}
|
|
62
76
|
|
|
63
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Load the street-morphology matcher: sealed artifact first, per-process dictionary build as the degrade path.
|
|
79
|
+
*/
|
|
64
80
|
export function loadStreetMorphologyFST(opts: LoadStreetMorphologyFSTOpts = {}): LoadedStreetMorphologyFST {
|
|
65
81
|
const warn = opts.onWarn ?? (() => {})
|
|
66
82
|
const artifactPath = opts.artifactPath ?? defaultStreetMorphologyArtifactPath()
|
|
@@ -72,9 +88,9 @@ export function loadStreetMorphologyFST(opts: LoadStreetMorphologyFSTOpts = {}):
|
|
|
72
88
|
const provenance = readFSTProvenance(buf)
|
|
73
89
|
|
|
74
90
|
return { matcher, source: "artifact", path: artifactPath, ...(provenance ? { provenance } : {}) }
|
|
75
|
-
} catch (
|
|
91
|
+
} catch (error) {
|
|
76
92
|
warn(
|
|
77
|
-
`street-morphology artifact at ${artifactPath} unreadable (${(
|
|
93
|
+
`street-morphology artifact at ${artifactPath} unreadable (${(error as Error).message}) — falling back to the dictionary build`
|
|
78
94
|
)
|
|
79
95
|
}
|
|
80
96
|
}
|
package/street-name-lookup.ts
CHANGED
|
@@ -39,9 +39,13 @@ function hasColumn(db: DatabaseSync, table: string, column: string): boolean {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
export interface SQLiteStreetNameLookupOpts {
|
|
42
|
-
/**
|
|
42
|
+
/**
|
|
43
|
+
* ISO-2 (upper-case) countries this index answers for. Default `["FR"]` (the BAN street-centroids instance).
|
|
44
|
+
*/
|
|
43
45
|
countries?: Iterable<string>
|
|
44
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Table name. Default `street_centroid`.
|
|
48
|
+
*/
|
|
45
49
|
table?: string
|
|
46
50
|
}
|
|
47
51
|
|
|
@@ -68,9 +72,11 @@ export class SQLiteStreetNameLookup implements StreetLocalityEvidence {
|
|
|
68
72
|
// `name_key` MUST match `foldStreetSurface` here (the fold-parity contract).
|
|
69
73
|
const keyCol = hasColumn(this.#db, table, "name_key") ? "name_key" : "street_norm"
|
|
70
74
|
this.#byName = this.#db.prepare(`SELECT 1 FROM ${table} WHERE ${keyCol} = ? LIMIT 1`)
|
|
75
|
+
|
|
71
76
|
this.#byNameLocality = this.#db.prepare(
|
|
72
77
|
`SELECT 1 FROM ${table} WHERE ${keyCol} = ? AND locality_base = ? LIMIT 1`
|
|
73
78
|
)
|
|
79
|
+
|
|
74
80
|
this.#byNamePostcode = this.#db.prepare(`SELECT 1 FROM ${table} WHERE ${keyCol} = ? AND postcode = ? LIMIT 1`)
|
|
75
81
|
}
|
|
76
82
|
}
|
|
@@ -83,18 +89,24 @@ export class SQLiteStreetNameLookup implements StreetLocalityEvidence {
|
|
|
83
89
|
|
|
84
90
|
// Scoped lookups tighten precision when the hypothesis carries a locality/postcode; a scoped MISS falls back to the
|
|
85
91
|
// unscoped probe (index incompleteness in the scope column is not evidence of absence — positive-evidence rule).
|
|
86
|
-
if (
|
|
87
|
-
|
|
92
|
+
if (
|
|
93
|
+
scope?.locality &&
|
|
94
|
+
this.#byNameLocality &&
|
|
95
|
+
this.#byNameLocality.get(norm, foldStreetSurface(scope.locality)) !== undefined
|
|
96
|
+
) {
|
|
97
|
+
return true
|
|
88
98
|
}
|
|
89
99
|
|
|
90
|
-
if (scope?.postcode && this.#byNamePostcode) {
|
|
91
|
-
|
|
100
|
+
if (scope?.postcode && this.#byNamePostcode && this.#byNamePostcode.get(norm, scope.postcode) !== undefined) {
|
|
101
|
+
return true
|
|
92
102
|
}
|
|
93
103
|
|
|
94
104
|
return this.#byName.get(norm) !== undefined
|
|
95
105
|
}
|
|
96
106
|
|
|
97
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Close the underlying handle.
|
|
109
|
+
*/
|
|
98
110
|
close(): void {
|
|
99
111
|
this.#db.close()
|
|
100
112
|
}
|
package/street-normalize.ts
CHANGED
|
@@ -24,6 +24,12 @@
|
|
|
24
24
|
|
|
25
25
|
import { AbbreviationToDirectional, US_STREET_SUFFIX_LOOKUP } from "@mailwoman/codex/us"
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Token count a street must exceed before its trailing pair is merged. At or below it the pair IS the whole street
|
|
29
|
+
* name, and merging would leave nothing to match on.
|
|
30
|
+
*/
|
|
31
|
+
const MIN_TOKENS_FOR_TAIL_MERGE = 3
|
|
32
|
+
|
|
27
33
|
/**
|
|
28
34
|
* Spelled ordinal street names → their digit-ordinal form ("tenth" → "10th"), applied ONLY when a street-type suffix
|
|
29
35
|
* follows (#723 admin-tail) — so the ordinal cross-streets common in grid cities ("Tenth Street", "Fifth Avenue") match
|
|
@@ -63,14 +69,16 @@ const SPELLED_ORDINAL_TO_DIGIT = new Map<string, string>([
|
|
|
63
69
|
["hundredth", "100th"],
|
|
64
70
|
])
|
|
65
71
|
|
|
66
|
-
/**
|
|
72
|
+
/**
|
|
73
|
+
* Lowercase + diacritic-fold + punctuation strip + whitespace collapse.
|
|
74
|
+
*/
|
|
67
75
|
function fold(input: string): string {
|
|
68
76
|
return input
|
|
69
77
|
.normalize("NFKD")
|
|
70
|
-
.
|
|
78
|
+
.replaceAll(/[̀-ͯ]/g, "")
|
|
71
79
|
.toLowerCase()
|
|
72
|
-
.
|
|
73
|
-
.
|
|
80
|
+
.replaceAll(/[.,'’]/g, "")
|
|
81
|
+
.replaceAll(/\s+/g, " ")
|
|
74
82
|
.trim()
|
|
75
83
|
}
|
|
76
84
|
|
|
@@ -81,7 +89,7 @@ function fold(input: string): string {
|
|
|
81
89
|
export function normalizeStreetForKey(street: string): string {
|
|
82
90
|
const tokens = fold(street).split(" ")
|
|
83
91
|
|
|
84
|
-
if (tokens.length
|
|
92
|
+
if (!tokens.length) return ""
|
|
85
93
|
|
|
86
94
|
// Spelled-ordinal street names → digit form when a street suffix follows ("Tenth Street" →
|
|
87
95
|
// "10th street", #723). Gated on the next token being a suffix so ordinal-WORD names are untouched.
|
|
@@ -99,6 +107,7 @@ export function normalizeStreetForKey(street: string): string {
|
|
|
99
107
|
// ("southeast"), and also merge an already-written two-token pair ("South East …").
|
|
100
108
|
const edgeDirectional = (raw: string) =>
|
|
101
109
|
AbbreviationToDirectional.get(raw.toUpperCase())?.toLowerCase().replace(" ", "")
|
|
110
|
+
|
|
102
111
|
const mergePair = (a?: string, b?: string) =>
|
|
103
112
|
a && b && /^(north|south)$/.test(a) && /^(east|west)$/.test(b) ? a + b : undefined
|
|
104
113
|
|
|
@@ -107,20 +116,21 @@ export function normalizeStreetForKey(street: string): string {
|
|
|
107
116
|
if (leadPair && tokens.length > 2) {
|
|
108
117
|
tokens.splice(0, 2, leadPair)
|
|
109
118
|
}
|
|
119
|
+
|
|
110
120
|
const first = edgeDirectional(tokens[0]!)
|
|
111
121
|
|
|
112
122
|
if (first && tokens.length > 1) {
|
|
113
123
|
tokens[0] = first
|
|
114
124
|
}
|
|
115
125
|
|
|
116
|
-
const tailPair = mergePair(tokens
|
|
126
|
+
const tailPair = mergePair(tokens.at(-2), tokens.at(-1))
|
|
117
127
|
|
|
118
|
-
if (tailPair && tokens.length >
|
|
119
|
-
tokens.splice(
|
|
128
|
+
if (tailPair && tokens.length > MIN_TOKENS_FOR_TAIL_MERGE) {
|
|
129
|
+
tokens.splice(-2, 2, tailPair)
|
|
120
130
|
}
|
|
121
131
|
|
|
122
132
|
if (tokens.length > 2) {
|
|
123
|
-
const last = edgeDirectional(tokens
|
|
133
|
+
const last = edgeDirectional(tokens.at(-1)!)
|
|
124
134
|
|
|
125
135
|
if (last) {
|
|
126
136
|
tokens[tokens.length - 1] = last
|
|
@@ -136,6 +146,7 @@ export function normalizeStreetForKey(street: string): string {
|
|
|
136
146
|
|
|
137
147
|
if (canonical) {
|
|
138
148
|
tokens[at] = canonical.toLowerCase()
|
|
149
|
+
|
|
139
150
|
break
|
|
140
151
|
}
|
|
141
152
|
}
|
|
@@ -196,9 +207,9 @@ export function normalizeStreetForKeyLocale(street: string, locale: StreetLocale
|
|
|
196
207
|
// hyphen ("Champs-Élysées", "St-Honoré") or a space — both sides fold identically, so this is pure
|
|
197
208
|
// robustness. It also splits a hyphenated abbreviation ("St-Honoré" → "st honore") into tokens the
|
|
198
209
|
// per-locale type/Saint map can see.
|
|
199
|
-
const tokens = fold(street).
|
|
210
|
+
const tokens = fold(street).replaceAll("ß", "ss").replaceAll("-", " ").split(/\s+/).filter(Boolean)
|
|
200
211
|
|
|
201
|
-
if (tokens.length
|
|
212
|
+
if (!tokens.length) return ""
|
|
202
213
|
|
|
203
214
|
switch (locale) {
|
|
204
215
|
case "fr":
|
|
@@ -229,7 +240,9 @@ export function normalizeStreetForKeyLocale(street: string, locale: StreetLocale
|
|
|
229
240
|
return tokens.join(" ")
|
|
230
241
|
}
|
|
231
242
|
|
|
232
|
-
/**
|
|
243
|
+
/**
|
|
244
|
+
* Normalize a locality name for address-point keying (fold only — no street semantics).
|
|
245
|
+
*/
|
|
233
246
|
export function normalizeLocalityForKey(locality: string): string {
|
|
234
247
|
return fold(locality)
|
|
235
248
|
}
|
|
@@ -268,7 +281,9 @@ export function stripLocalityQualifier(locality: string): string {
|
|
|
268
281
|
|
|
269
282
|
if (s.includes("/")) {
|
|
270
283
|
s = s.split("/")[0]!.trim()
|
|
271
|
-
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// "Kraubath/Mur", "St.Kanzian/Klopeiner See"
|
|
272
287
|
s = s.replace(/\s+[a-zà-ÿ]\.\s*\S.*$/iu, "") // abbreviated " b.Graz" / " o.Bleiburg" / " a.d. …"
|
|
273
288
|
s = s.replace(/\s+(im|an der|ob|bei|in der|unter|vor)\s+\S.*$/iu, "") // " im Simmental", " bei Graz"
|
|
274
289
|
s = s.replace(/\s+(S|N|E|W|V|Ø|Sø|Fyn|Thy|Sjælland|Jylland|[A-ZÅÄÖ]{2})$/u, "") // " S", " VD", " Thy"
|
package/street-segment-schema.ts
CHANGED
|
@@ -23,28 +23,48 @@ import type { Kysely } from "kysely"
|
|
|
23
23
|
* `odd`/`even`/`mixed`.
|
|
24
24
|
*/
|
|
25
25
|
export interface StreetSegmentTable {
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Shared {@link normalizeStreetForKey} of the street — the build/query-consistent probe key.
|
|
28
|
+
*/
|
|
27
29
|
street_norm: string
|
|
28
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* `L` or `R` — the TIGER side the address range sits on.
|
|
32
|
+
*/
|
|
29
33
|
side: string
|
|
30
34
|
from_hn: number
|
|
31
35
|
to_hn: number
|
|
32
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* Sorted lower bound of `(from_hn, to_hn)` — the probe filters `min_hn <= n <= max_hn`.
|
|
38
|
+
*/
|
|
33
39
|
min_hn: number
|
|
34
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Sorted upper bound of `(from_hn, to_hn)`.
|
|
42
|
+
*/
|
|
35
43
|
max_hn: number
|
|
36
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* `odd` | `even` | `mixed` — the house-number parity along the range.
|
|
46
|
+
*/
|
|
37
47
|
parity: string
|
|
38
48
|
postcode: string | null
|
|
39
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* 5-digit state+county FIPS the edge came from.
|
|
51
|
+
*/
|
|
40
52
|
county_fips: string
|
|
41
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* The street as it appeared in TIGER (kept for display / debugging).
|
|
55
|
+
*/
|
|
42
56
|
street_raw: string
|
|
43
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* GeoJSON LineString text (no SpatiaLite — read back with `JSON.parse`).
|
|
59
|
+
*/
|
|
44
60
|
geometry: string
|
|
45
|
-
/**
|
|
61
|
+
/**
|
|
62
|
+
* Provenance: the dataset this edge came from (e.g. `tiger:edges`).
|
|
63
|
+
*/
|
|
46
64
|
source: string
|
|
47
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* The pinned TIGER release the edge was ingested from.
|
|
67
|
+
*/
|
|
48
68
|
release: string
|
|
49
69
|
}
|
|
50
70
|
|
|
@@ -56,15 +76,23 @@ export interface StreetSegmentTable {
|
|
|
56
76
|
* and callers fall back to the in-code per-region table (never patch shipped DBs — rebuild).
|
|
57
77
|
*/
|
|
58
78
|
export interface InterpCalibrationRow {
|
|
59
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Conformal multiplier for the raw half-segment `uncertainty_m` radius (#374/#584) — ×Q̂ for a ~90% bound.
|
|
81
|
+
*/
|
|
60
82
|
radius_multiplier: number
|
|
61
|
-
/**
|
|
83
|
+
/**
|
|
84
|
+
* Provenance of the multiplier (e.g. `split-conformal:2026-06-14`).
|
|
85
|
+
*/
|
|
62
86
|
method: string
|
|
63
|
-
/**
|
|
87
|
+
/**
|
|
88
|
+
* The calibration-table key the multiplier was selected by (a USPS region code, or `default` for unmeasured).
|
|
89
|
+
*/
|
|
64
90
|
region: string
|
|
65
91
|
}
|
|
66
92
|
|
|
67
|
-
/**
|
|
93
|
+
/**
|
|
94
|
+
* The street-segment database schema for `new DatabaseClient<StreetSegmentDatabase>(...)`.
|
|
95
|
+
*/
|
|
68
96
|
export interface StreetSegmentDatabase {
|
|
69
97
|
street_segment: StreetSegmentTable
|
|
70
98
|
interp_calibration: InterpCalibrationRow
|
|
@@ -90,7 +118,9 @@ export const STREET_SEGMENT_COLUMNS = [
|
|
|
90
118
|
"release",
|
|
91
119
|
] as const
|
|
92
120
|
|
|
93
|
-
/**
|
|
121
|
+
/**
|
|
122
|
+
* Create the `street_segment` table — called before the streaming bulk load.
|
|
123
|
+
*/
|
|
94
124
|
export async function createStreetSegmentTable(db: Kysely<StreetSegmentDatabase>): Promise<void> {
|
|
95
125
|
await db.schema
|
|
96
126
|
.createTable("street_segment")
|
|
@@ -126,15 +156,19 @@ export async function writeInterpCalibration(
|
|
|
126
156
|
.addColumn("method", "text", (c) => c.notNull())
|
|
127
157
|
.addColumn("region", "text", (c) => c.notNull())
|
|
128
158
|
.execute()
|
|
159
|
+
|
|
129
160
|
await db.insertInto("interp_calibration").values(row).execute()
|
|
130
161
|
}
|
|
131
162
|
|
|
132
|
-
/**
|
|
163
|
+
/**
|
|
164
|
+
* Create the two probe indexes the reader relies on (postcode-scope, street-scope).
|
|
165
|
+
*/
|
|
133
166
|
export async function createStreetSegmentIndexes(db: Kysely<StreetSegmentDatabase>): Promise<void> {
|
|
134
167
|
await db.schema
|
|
135
168
|
.createIndex("idx_seg_postcode")
|
|
136
169
|
.on("street_segment")
|
|
137
170
|
.columns(["postcode", "street_norm", "min_hn"])
|
|
138
171
|
.execute()
|
|
172
|
+
|
|
139
173
|
await db.schema.createIndex("idx_seg_street").on("street_segment").columns(["street_norm", "min_hn"]).execute()
|
|
140
174
|
}
|
package/types.ts
CHANGED
|
@@ -49,7 +49,9 @@ export interface PlaceCandidate {
|
|
|
49
49
|
id: number
|
|
50
50
|
name: string
|
|
51
51
|
placetype: WOFPlacetype
|
|
52
|
-
/**
|
|
52
|
+
/**
|
|
53
|
+
* ISO 3166-1 alpha-2 country code.
|
|
54
|
+
*/
|
|
53
55
|
country: string
|
|
54
56
|
lat: number
|
|
55
57
|
lon: number
|
|
@@ -125,9 +127,13 @@ export interface GeoBbox {
|
|
|
125
127
|
export interface FindPlaceQuery {
|
|
126
128
|
text: string
|
|
127
129
|
placetype?: WOFPlacetype | WOFPlacetype[]
|
|
128
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* ISO 3166-1 alpha-2 — narrows to one country.
|
|
132
|
+
*/
|
|
129
133
|
country?: string
|
|
130
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* WOF place id — narrows to descendants of this place.
|
|
136
|
+
*/
|
|
131
137
|
parentID?: number
|
|
132
138
|
/**
|
|
133
139
|
* Sibling postcode. When set on a `locality` query AND a `postcode_locality` table is present, triggers the
|
|
@@ -136,7 +142,9 @@ export interface FindPlaceQuery {
|
|
|
136
142
|
* postcode_locality shard is present.
|
|
137
143
|
*/
|
|
138
144
|
postcode?: string
|
|
139
|
-
/**
|
|
145
|
+
/**
|
|
146
|
+
* Proximity hint — candidates close to this point get a ranking boost.
|
|
147
|
+
*/
|
|
140
148
|
near?: GeoPoint & { maxDistanceKm?: number }
|
|
141
149
|
/**
|
|
142
150
|
* Ordered proximity-bias points (viewport center, user location, …), each optionally weighted (default 1.0, first
|
|
@@ -147,9 +155,13 @@ export interface FindPlaceQuery {
|
|
|
147
155
|
* back-compat.
|
|
148
156
|
*/
|
|
149
157
|
bias?: Array<GeoPoint & { weight?: number }>
|
|
150
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Bounding-box filter — only candidates whose bbox intersects this box are returned.
|
|
160
|
+
*/
|
|
151
161
|
bbox?: GeoBbox
|
|
152
|
-
/**
|
|
162
|
+
/**
|
|
163
|
+
* Default 10.
|
|
164
|
+
*/
|
|
153
165
|
limit?: number
|
|
154
166
|
}
|
|
155
167
|
|
package/unified-schema.ts
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* `place_search` FTS5 + `place_bbox` R*Tree are built separately by `build-fts` (fts.ts).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
|
-
import { DatabaseSync } from "node:sqlite"
|
|
15
|
+
import type { DatabaseSync } from "node:sqlite"
|
|
16
16
|
|
|
17
17
|
import { DatabaseClient } from "@mailwoman/core/kysley/client"
|
|
18
18
|
|
|
@@ -109,11 +109,13 @@ export async function createUnifiedSchema(db: DatabaseSync): Promise<void> {
|
|
|
109
109
|
*/
|
|
110
110
|
export function populateAncestors(db: DatabaseSync): number {
|
|
111
111
|
db.exec("DELETE FROM ancestors")
|
|
112
|
+
|
|
112
113
|
const rows = db.prepare("SELECT id, parent_id, placetype FROM spr").all() as Array<{
|
|
113
114
|
id: number
|
|
114
115
|
parent_id: number
|
|
115
116
|
placetype: string
|
|
116
117
|
}>
|
|
118
|
+
|
|
117
119
|
const byID = new Map<number, { parent: number; placetype: string }>()
|
|
118
120
|
|
|
119
121
|
for (const r of rows) {
|
|
@@ -125,7 +127,9 @@ export function populateAncestors(db: DatabaseSync): number {
|
|
|
125
127
|
let count = 0
|
|
126
128
|
|
|
127
129
|
for (const r of rows) {
|
|
128
|
-
insert.run(r.id, r.id, r.placetype)
|
|
130
|
+
insert.run(r.id, r.id, r.placetype)
|
|
131
|
+
|
|
132
|
+
// self
|
|
129
133
|
count++
|
|
130
134
|
const seen = new Set<number>([r.id])
|
|
131
135
|
let cur = r.parent_id
|
|
@@ -135,11 +139,13 @@ export function populateAncestors(db: DatabaseSync): number {
|
|
|
135
139
|
|
|
136
140
|
if (!node) break
|
|
137
141
|
insert.run(r.id, cur, node.placetype)
|
|
142
|
+
|
|
138
143
|
count++
|
|
139
144
|
seen.add(cur)
|
|
140
145
|
cur = node.parent
|
|
141
146
|
}
|
|
142
147
|
}
|
|
148
|
+
|
|
143
149
|
db.exec("COMMIT")
|
|
144
150
|
|
|
145
151
|
return count
|
|
@@ -152,18 +158,21 @@ export async function createUnifiedIndexes(db: DatabaseSync): Promise<void> {
|
|
|
152
158
|
await kdb.schema.createIndex("spr_by_parent").ifNotExists().on("spr").column("parent_id").execute()
|
|
153
159
|
await kdb.schema.createIndex("names_by_id").ifNotExists().on("names").column("id").execute()
|
|
154
160
|
await kdb.schema.createIndex("names_by_name").ifNotExists().on("names").column("name").execute()
|
|
161
|
+
|
|
155
162
|
await kdb.schema
|
|
156
163
|
.createIndex("concordances_by_id")
|
|
157
164
|
.ifNotExists()
|
|
158
165
|
.on("concordances")
|
|
159
166
|
.columns(["id", "lastmodified"])
|
|
160
167
|
.execute()
|
|
168
|
+
|
|
161
169
|
await kdb.schema
|
|
162
170
|
.createIndex("concordances_by_other_id")
|
|
163
171
|
.ifNotExists()
|
|
164
172
|
.on("concordances")
|
|
165
173
|
.columns(["other_source", "other_id"])
|
|
166
174
|
.execute()
|
|
175
|
+
|
|
167
176
|
// ancestor_id is the hot column (parent-constraint queries `WHERE ancestor_id = ?`); id supports
|
|
168
177
|
// the reverse lookup.
|
|
169
178
|
await kdb.schema.createIndex("ancestors_by_ancestor").ifNotExists().on("ancestors").column("ancestor_id").execute()
|