@mailwoman/resolver-wof-sqlite 8.0.0 → 8.2.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 +49 -15
- package/candidate-schema.ts +35 -11
- package/coincident-roles.ts +28 -6
- package/convention.ts +3 -1
- package/coverage-manifest-schema.ts +243 -0
- package/fst-autocomplete.ts +15 -9
- package/fst-builder.ts +65 -9
- package/fst-deserialize-web.ts +46 -9
- package/fst-matcher.ts +12 -5
- package/fst-serialize.ts +83 -9
- package/fst-types.ts +43 -0
- 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 +19 -0
- package/interpolation.ts +59 -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 +16 -3
- package/out/candidate-lookup.d.ts.map +1 -1
- package/out/candidate-lookup.js +27 -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 +112 -0
- package/out/coverage-manifest-schema.d.ts.map +1 -0
- package/out/coverage-manifest-schema.js +154 -0
- package/out/coverage-manifest-schema.js.map +1 -0
- 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 +42 -9
- 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 +43 -0
- 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 +2 -0
- package/out/index.d.ts.map +1 -1
- package/out/index.js +1 -0
- package/out/index.js.map +1 -1
- package/out/interpolation.d.ts +24 -6
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +32 -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 +67 -0
- package/out/street-morphology-fst-loader.d.ts.map +1 -0
- package/out/street-morphology-fst-loader.js +59 -0
- package/out/street-morphology-fst-loader.js.map +1 -0
- 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 +68 -13
- package/out/street-segment-schema.d.ts.map +1 -1
- package/out/street-segment-schema.js +21 -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 +13 -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 +103 -0
- package/street-name-lookup.ts +19 -7
- package/street-normalize.ts +28 -13
- package/street-segment-schema.ts +83 -13
- package/types.ts +18 -6
- package/unified-schema.ts +11 -2
package/reverse.ts
CHANGED
|
@@ -35,6 +35,16 @@ import { PLACE_BBOX_TABLE } from "./fts.ts"
|
|
|
35
35
|
import { geometryContains, haversineKm, type GeojsonGeometry } from "./geo.ts"
|
|
36
36
|
import type { PlaceCandidate, WOFPlacetype } from "./types.ts"
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Largest absolute latitude in WGS-84 degrees.
|
|
40
|
+
*/
|
|
41
|
+
const MAX_ABS_LATITUDE = 90
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Largest absolute longitude in WGS-84 degrees.
|
|
45
|
+
*/
|
|
46
|
+
const MAX_ABS_LONGITUDE = 180
|
|
47
|
+
|
|
38
48
|
/**
|
|
39
49
|
* How the deepest returned place was confirmed:
|
|
40
50
|
*
|
|
@@ -52,7 +62,9 @@ export interface ReverseGeocodeResult {
|
|
|
52
62
|
* ocean, or outside the gazetteer's coverage).
|
|
53
63
|
*/
|
|
54
64
|
hierarchy: PlaceCandidate[]
|
|
55
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* Containment kind of the DEEPEST place in `hierarchy` (see {@link ContainmentKind}).
|
|
67
|
+
*/
|
|
56
68
|
containment: ContainmentKind
|
|
57
69
|
}
|
|
58
70
|
|
|
@@ -62,14 +74,18 @@ export interface WOFReverseGeocoderOpts {
|
|
|
62
74
|
* package-built `place_bbox` R*Tree (`mailwoman gazetteer build fts`). Mutually exclusive with `adminDatabase`.
|
|
63
75
|
*/
|
|
64
76
|
adminDBPath?: string
|
|
65
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* Pre-opened admin DB — primarily for tests against an inline fixture.
|
|
79
|
+
*/
|
|
66
80
|
adminDatabase?: DatabaseSync
|
|
67
81
|
/**
|
|
68
82
|
* Path to the polygon sidecar DB (`wof-polygons.db`, table `polygons(id, geom)`). OPTIONAL — without it every result
|
|
69
83
|
* is `containment: "approximate"` (centroid-only mode). Mutually exclusive with `polygonDatabase`.
|
|
70
84
|
*/
|
|
71
85
|
polygonDBPath?: string
|
|
72
|
-
/**
|
|
86
|
+
/**
|
|
87
|
+
* Pre-opened polygon DB — primarily for tests.
|
|
88
|
+
*/
|
|
73
89
|
polygonDatabase?: DatabaseSync
|
|
74
90
|
}
|
|
75
91
|
|
|
@@ -108,7 +124,9 @@ const DESCENT_TIERS: readonly WOFPlacetype[] = [
|
|
|
108
124
|
"microhood",
|
|
109
125
|
]
|
|
110
126
|
|
|
111
|
-
/**
|
|
127
|
+
/**
|
|
128
|
+
* Internal candidate row off `spr` (+ optional bbox area / centroid distance bookkeeping).
|
|
129
|
+
*/
|
|
112
130
|
interface CandidateRow {
|
|
113
131
|
id: number
|
|
114
132
|
name: string
|
|
@@ -167,8 +185,10 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
167
185
|
|
|
168
186
|
this.#admin = opts.adminDatabase ?? new DatabaseSync(opts.adminDBPath!, { readOnly: true })
|
|
169
187
|
this.#ownsAdmin = !opts.adminDatabase
|
|
188
|
+
|
|
170
189
|
this.#polygons =
|
|
171
190
|
opts.polygonDatabase ?? (opts.polygonDBPath ? new DatabaseSync(opts.polygonDBPath, { readOnly: true }) : null)
|
|
191
|
+
|
|
172
192
|
this.#ownsPolygons = !opts.polygonDatabase && Boolean(opts.polygonDBPath)
|
|
173
193
|
|
|
174
194
|
// Fail loudly up front — the R*Tree is a build artifact, not part of the upstream WOF
|
|
@@ -213,9 +233,15 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
213
233
|
* whose `POIIntentOutcome` return type is synchronous by contract — see `poi-intent.ts`'s `deps.execute`).
|
|
214
234
|
*/
|
|
215
235
|
reverseGeocodeSync(lat: number, lon: number, opts: ReverseGeocodeOpts = {}): ReverseGeocodeResult {
|
|
216
|
-
if (
|
|
236
|
+
if (
|
|
237
|
+
!Number.isFinite(lat) ||
|
|
238
|
+
!Number.isFinite(lon) ||
|
|
239
|
+
Math.abs(lat) > MAX_ABS_LATITUDE ||
|
|
240
|
+
Math.abs(lon) > MAX_ABS_LONGITUDE
|
|
241
|
+
) {
|
|
217
242
|
throw new RangeError(`WOFReverseGeocoder.reverseGeocode: (${lat}, ${lon}) is not a WGS-84 coordinate`)
|
|
218
243
|
}
|
|
244
|
+
|
|
219
245
|
const maxApproximateKm = opts.maxApproximateKm ?? DEFAULT_MAX_APPROXIMATE_KM
|
|
220
246
|
const candidates = this.#bboxCandidates(lat, lon, opts)
|
|
221
247
|
|
|
@@ -231,6 +257,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
231
257
|
if (contains === true) {
|
|
232
258
|
winner = c
|
|
233
259
|
winnerConfirmed = true
|
|
260
|
+
|
|
234
261
|
break
|
|
235
262
|
}
|
|
236
263
|
|
|
@@ -277,6 +304,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
277
304
|
next = k
|
|
278
305
|
nextConfirmed = true
|
|
279
306
|
nextKm = undefined
|
|
307
|
+
|
|
280
308
|
break
|
|
281
309
|
}
|
|
282
310
|
|
|
@@ -302,8 +330,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
302
330
|
// Hierarchy assembly via the shared ancestor walk. If the descent crossed an ancestry gap
|
|
303
331
|
// (the deepest place's recorded lineage misses the PIP root), merge the root's own chain so
|
|
304
332
|
// region/country are always present when a polygon confirmed them.
|
|
305
|
-
const byID = new Map<number, PlaceCandidate>()
|
|
306
|
-
byID.set(current.id, toPlaceCandidate(current, currentDistanceKm))
|
|
333
|
+
const byID = new Map<number, PlaceCandidate>([[current.id, toPlaceCandidate(current, currentDistanceKm)]])
|
|
307
334
|
|
|
308
335
|
for (const a of ancestorLineage(this.#admin, current.id)) {
|
|
309
336
|
if (!byID.has(a.id)) {
|
|
@@ -320,6 +347,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
320
347
|
}
|
|
321
348
|
}
|
|
322
349
|
}
|
|
350
|
+
|
|
323
351
|
const hierarchy = [...byID.values()]
|
|
324
352
|
|
|
325
353
|
if (opts.placetypes) {
|
|
@@ -331,12 +359,15 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
331
359
|
}
|
|
332
360
|
}
|
|
333
361
|
}
|
|
362
|
+
|
|
334
363
|
hierarchy.sort((a, b) => placetypeDepth(b.placetype) - placetypeDepth(a.placetype))
|
|
335
364
|
|
|
336
365
|
return { hierarchy, containment: currentConfirmed ? "polygon" : "approximate" }
|
|
337
366
|
}
|
|
338
367
|
|
|
339
|
-
/**
|
|
368
|
+
/**
|
|
369
|
+
* Bbox candidates containing the point, smallest-area-first, via the `place_bbox` R*Tree.
|
|
370
|
+
*/
|
|
340
371
|
#bboxCandidates(lat: number, lon: number, opts: ReverseGeocodeOpts): CandidateRow[] {
|
|
341
372
|
const where: string[] = [
|
|
342
373
|
"bbox.min_lat <= ?",
|
|
@@ -346,12 +377,14 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
346
377
|
"spr.is_current != 0",
|
|
347
378
|
"spr.is_deprecated = 0",
|
|
348
379
|
]
|
|
380
|
+
|
|
349
381
|
const params: Array<number | string> = [lat, lat, lon, lon]
|
|
350
382
|
|
|
351
|
-
if (opts.placetypes && opts.placetypes.length
|
|
383
|
+
if (opts.placetypes && opts.placetypes.length) {
|
|
352
384
|
where.push(`spr.placetype IN (${opts.placetypes.map(() => "?").join(", ")})`)
|
|
353
385
|
params.push(...opts.placetypes)
|
|
354
386
|
}
|
|
387
|
+
|
|
355
388
|
params.push(opts.maxCandidates ?? DEFAULT_MAX_CANDIDATES)
|
|
356
389
|
|
|
357
390
|
return this.#admin
|
|
@@ -398,7 +431,9 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
398
431
|
) as unknown as CandidateRow[]
|
|
399
432
|
}
|
|
400
433
|
|
|
401
|
-
/**
|
|
434
|
+
/**
|
|
435
|
+
* Parsed GeoJSON geometry for a WOF id, or null when absent / unparseable / no polygon DB.
|
|
436
|
+
*/
|
|
402
437
|
#geometry(id: number): GeojsonGeometry | null {
|
|
403
438
|
if (!this.#polygons) return null
|
|
404
439
|
const cached = this.#geometryCache.get(id)
|
|
@@ -408,6 +443,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
408
443
|
if (this.#geometryCache.size >= WOFReverseGeocoder.#GEOMETRY_CACHE_CAP) {
|
|
409
444
|
this.#geometryCache.clear()
|
|
410
445
|
}
|
|
446
|
+
|
|
411
447
|
const row = this.#polygons.prepare(`SELECT geom FROM polygons WHERE id = ?`).get(id) as { geom: string } | undefined
|
|
412
448
|
let geometry: GeojsonGeometry | null = null
|
|
413
449
|
|
|
@@ -418,6 +454,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
418
454
|
geometry = null // malformed row — treat as no-polygon rather than failing the query
|
|
419
455
|
}
|
|
420
456
|
}
|
|
457
|
+
|
|
421
458
|
this.#geometryCache.set(id, geometry)
|
|
422
459
|
|
|
423
460
|
return geometry
|
package/sharding.ts
CHANGED
|
@@ -47,7 +47,7 @@ export function deriveSchemaName(path: string): string {
|
|
|
47
47
|
.replace(/^whosonfirst-data-/u, "")
|
|
48
48
|
.replace(/-latest\.db$/u, "")
|
|
49
49
|
.replace(/\.db$/u, "")
|
|
50
|
-
.
|
|
50
|
+
.replaceAll(/[^a-zA-Z0-9_]/g, "_")
|
|
51
51
|
|
|
52
52
|
if (!stem) {
|
|
53
53
|
throw new Error(`deriveSchemaName: could not derive a SQL schema name from path ${JSON.stringify(path)}`)
|
|
@@ -86,7 +86,9 @@ export interface ResolvedShard {
|
|
|
86
86
|
placetypes: readonly string[]
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
/**
|
|
89
|
+
/**
|
|
90
|
+
* SQLite identifier regex — `[A-Za-z_][A-Za-z0-9_]*`.
|
|
91
|
+
*/
|
|
90
92
|
const SQLITE_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/u
|
|
91
93
|
|
|
92
94
|
/**
|
|
@@ -99,7 +101,7 @@ const SQLITE_IDENT_RE = /^[A-Za-z_][A-Za-z0-9_]*$/u
|
|
|
99
101
|
export function resolveShards(input: string | ReadonlyArray<string | ShardConfig>): ResolvedShard[] {
|
|
100
102
|
const list = typeof input === "string" ? [input] : input
|
|
101
103
|
|
|
102
|
-
if (list.length
|
|
104
|
+
if (!list.length) throw new Error("resolveShards: at least one shard is required")
|
|
103
105
|
|
|
104
106
|
const seen = new Set<string>()
|
|
105
107
|
const out: ResolvedShard[] = []
|
|
@@ -116,6 +118,7 @@ export function resolveShards(input: string | ReadonlyArray<string | ShardConfig
|
|
|
116
118
|
`{ path, schemaName } to override.`
|
|
117
119
|
)
|
|
118
120
|
}
|
|
121
|
+
|
|
119
122
|
// The first shard is always main per SQLite semantics — its derived name is informational
|
|
120
123
|
// only. Subsequent shards must have unique non-main names.
|
|
121
124
|
const schemaName = i === 0 ? "main" : derived
|
|
@@ -126,7 +129,9 @@ export function resolveShards(input: string | ReadonlyArray<string | ShardConfig
|
|
|
126
129
|
`(either with "main" or another shard). Pass an explicit { path, schemaName }.`
|
|
127
130
|
)
|
|
128
131
|
}
|
|
132
|
+
|
|
129
133
|
seen.add(schemaName)
|
|
134
|
+
|
|
130
135
|
out.push({
|
|
131
136
|
path: cfg.path,
|
|
132
137
|
schemaName,
|
|
@@ -179,7 +184,7 @@ export function pickShardsForPlacetype(shards: ResolvedShard[], placetype: strin
|
|
|
179
184
|
}
|
|
180
185
|
}
|
|
181
186
|
|
|
182
|
-
return matches.length
|
|
187
|
+
return matches.length ? matches : [shards[0]!]
|
|
183
188
|
}
|
|
184
189
|
|
|
185
190
|
export function pickShardForPlacetype(
|
|
@@ -194,7 +199,9 @@ export function pickShardForPlacetype(
|
|
|
194
199
|
* order remains the tiebreak when no shard claims the country (or none was probed).
|
|
195
200
|
*/
|
|
196
201
|
country?: string
|
|
197
|
-
/**
|
|
202
|
+
/**
|
|
203
|
+
* Per-schema probed country sets (see `WOFSqlitePlaceLookup`'s construction probe).
|
|
204
|
+
*/
|
|
198
205
|
countriesBySchema?: ReadonlyMap<string, ReadonlySet<string>>
|
|
199
206
|
}
|
|
200
207
|
): ResolvedShard {
|
|
@@ -223,7 +230,7 @@ export function pickShardForPlacetype(
|
|
|
223
230
|
}
|
|
224
231
|
}
|
|
225
232
|
|
|
226
|
-
if (matches.length
|
|
233
|
+
if (!matches.length) return shards[0]!
|
|
227
234
|
|
|
228
235
|
if (opts?.country && opts.countriesBySchema) {
|
|
229
236
|
for (const s of matches) {
|
|
@@ -22,7 +22,9 @@ import { ADDRESS_CONVENTION_TABLE, type Convention, type ConventionSource } from
|
|
|
22
22
|
export class SqliteConventionSource implements ConventionSource {
|
|
23
23
|
readonly #db: DatabaseSync
|
|
24
24
|
readonly #schema: string
|
|
25
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Memoize per-id lookups (including misses, as `null`) so a hot ancestor chain is queried once.
|
|
27
|
+
*/
|
|
26
28
|
readonly #cache = new Map<number, Convention | null>()
|
|
27
29
|
|
|
28
30
|
/**
|
|
@@ -54,6 +56,7 @@ export class SqliteConventionSource implements ConventionSource {
|
|
|
54
56
|
// WORLD_DEFAULT). The build script validates structure, so this is purely defensive.
|
|
55
57
|
value = null
|
|
56
58
|
}
|
|
59
|
+
|
|
57
60
|
this.#cache.set(wofID, value)
|
|
58
61
|
|
|
59
62
|
return value ?? undefined
|
|
@@ -29,27 +29,45 @@ import type { Kysely } from "kysely"
|
|
|
29
29
|
* bbox diagonal into an honest `uncertainty_m`).
|
|
30
30
|
*/
|
|
31
31
|
export interface StreetCentroidTable {
|
|
32
|
-
/**
|
|
32
|
+
/**
|
|
33
|
+
* Shared `normalizeStreetForKeyLocale` of the street — the build/query-consistent probe key.
|
|
34
|
+
*/
|
|
33
35
|
street_norm: string
|
|
34
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The 5-digit postcode of this group, or null when the source row carried none.
|
|
38
|
+
*/
|
|
35
39
|
postcode: string | null
|
|
36
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Arrondissement-stripped commune (`stripArrondissement(normalizeLocalityForKey(commune))`) — the fallback scope.
|
|
42
|
+
*/
|
|
37
43
|
locality_base: string
|
|
38
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* Weighted-mean centroid latitude of the street's member points.
|
|
46
|
+
*/
|
|
39
47
|
lat: number
|
|
40
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Weighted-mean centroid longitude of the street's member points.
|
|
50
|
+
*/
|
|
41
51
|
lon: number
|
|
42
52
|
min_lat: number
|
|
43
53
|
max_lat: number
|
|
44
54
|
min_lon: number
|
|
45
55
|
max_lon: number
|
|
46
|
-
/**
|
|
56
|
+
/**
|
|
57
|
+
* Member address-point count — the weight for a cross-group centroid aggregate.
|
|
58
|
+
*/
|
|
47
59
|
point_count: number
|
|
48
|
-
/**
|
|
60
|
+
/**
|
|
61
|
+
* A representative street name as it appeared in the source (display / debugging).
|
|
62
|
+
*/
|
|
49
63
|
street_raw: string
|
|
50
|
-
/**
|
|
64
|
+
/**
|
|
65
|
+
* Provenance: the register this street was derived from (e.g. `ban:fr`).
|
|
66
|
+
*/
|
|
51
67
|
source: string
|
|
52
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* The pinned data release the underlying points came from.
|
|
70
|
+
*/
|
|
53
71
|
release: string
|
|
54
72
|
/**
|
|
55
73
|
* #727 phase-4c: `foldStreetSurface(street_raw)` — the contract-fold street-NAME existence key for
|
|
@@ -61,7 +79,9 @@ export interface StreetCentroidTable {
|
|
|
61
79
|
name_key: string
|
|
62
80
|
}
|
|
63
81
|
|
|
64
|
-
/**
|
|
82
|
+
/**
|
|
83
|
+
* The street-centroid database schema for `new DatabaseClient<StreetCentroidDatabase>(...)`.
|
|
84
|
+
*/
|
|
65
85
|
export interface StreetCentroidDatabase {
|
|
66
86
|
street_centroid: StreetCentroidTable
|
|
67
87
|
}
|
|
@@ -87,7 +107,9 @@ export const STREET_CENTROID_COLUMNS = [
|
|
|
87
107
|
"name_key",
|
|
88
108
|
] as const
|
|
89
109
|
|
|
90
|
-
/**
|
|
110
|
+
/**
|
|
111
|
+
* Create the `street_centroid` table — called before the streaming bulk load.
|
|
112
|
+
*/
|
|
91
113
|
export async function createStreetCentroidTable(db: Kysely<StreetCentroidDatabase>): Promise<void> {
|
|
92
114
|
await db.schema
|
|
93
115
|
.createTable("street_centroid")
|
|
@@ -115,10 +137,12 @@ export async function createStreetCentroidTable(db: Kysely<StreetCentroidDatabas
|
|
|
115
137
|
*/
|
|
116
138
|
export async function createStreetCentroidIndexes(db: Kysely<StreetCentroidDatabase>): Promise<void> {
|
|
117
139
|
await db.schema.createIndex("idx_sc_postcode").on("street_centroid").columns(["postcode", "street_norm"]).execute()
|
|
140
|
+
|
|
118
141
|
await db.schema
|
|
119
142
|
.createIndex("idx_sc_locality")
|
|
120
143
|
.on("street_centroid")
|
|
121
144
|
.columns(["locality_base", "street_norm"])
|
|
122
145
|
.execute()
|
|
146
|
+
|
|
123
147
|
await db.schema.createIndex("idx_sc_name").on("street_centroid").columns(["name_key"]).execute()
|
|
124
148
|
}
|
package/street-centroid.ts
CHANGED
|
@@ -31,7 +31,9 @@ import {
|
|
|
31
31
|
stripArrondissement,
|
|
32
32
|
} from "./street-normalize.ts"
|
|
33
33
|
|
|
34
|
-
/**
|
|
34
|
+
/**
|
|
35
|
+
* The weighted-centroid + extent + provenance an aggregate probe projects. `lat` is null when nothing matched.
|
|
36
|
+
*/
|
|
35
37
|
interface AggRow {
|
|
36
38
|
lat: number | null
|
|
37
39
|
lon: number | null
|
|
@@ -43,14 +45,18 @@ interface AggRow {
|
|
|
43
45
|
release: string | null
|
|
44
46
|
}
|
|
45
47
|
|
|
46
|
-
/**
|
|
48
|
+
/**
|
|
49
|
+
* Weighted-centroid aggregate over a WHERE-filtered set. `SUM(coord*n)/SUM(n)` reconstructs the grand centroid.
|
|
50
|
+
*/
|
|
47
51
|
const AGG_SELECT =
|
|
48
52
|
"SUM(lat * point_count) / SUM(point_count) AS lat, " +
|
|
49
53
|
"SUM(lon * point_count) / SUM(point_count) AS lon, " +
|
|
50
54
|
"MIN(min_lat) AS min_lat, MAX(max_lat) AS max_lat, MIN(min_lon) AS min_lon, MAX(max_lon) AS max_lon, " +
|
|
51
55
|
"MAX(source) AS source, MAX(release) AS release"
|
|
52
56
|
|
|
53
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* Half the bbox diagonal, in METERS — an honest coarse radius for a street centroid.
|
|
59
|
+
*/
|
|
54
60
|
function extentRadiusM(minLat: number, maxLat: number, minLon: number, maxLon: number): number {
|
|
55
61
|
const R = 6_371_000
|
|
56
62
|
const toRad = (d: number): number => (d * Math.PI) / 180
|
|
@@ -84,6 +90,7 @@ export class StreetCentroidSqliteLookup implements StreetCentroidLookup {
|
|
|
84
90
|
this.#byPostcode = this.#db.prepare(
|
|
85
91
|
`SELECT ${AGG_SELECT} FROM street_centroid WHERE postcode = ? AND street_norm = ?`
|
|
86
92
|
)
|
|
93
|
+
|
|
87
94
|
this.#byLocality = this.#db.prepare(
|
|
88
95
|
`SELECT ${AGG_SELECT} FROM street_centroid WHERE locality_base = ? AND street_norm = ?`
|
|
89
96
|
)
|
|
@@ -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.
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Unified runtime loader for the street-morphology FST — the #1315 street-context gate's signal
|
|
7
|
+
* source, previously rebuilt from the libpostal dictionaries per process at three duplicate call
|
|
8
|
+
* sites (runtime pipeline / parity eval / neural harness). The sealed artifact
|
|
9
|
+
* (`fst-street-morphology.bin`, built by `mailwoman gazetteer build street-morphology`) replaces
|
|
10
|
+
* those per-process builds; this loader is the ONE resolution ladder every node call site shares:
|
|
11
|
+
*
|
|
12
|
+
* 1. An explicit `artifactPath` (e.g. the weights-package sibling surfaced by
|
|
13
|
+
* `NeuralAddressClassifier.streetMorphologyPath`). When given, it is the only artifact probed.
|
|
14
|
+
* 2. Otherwise the staged sealed artifact at `$MAILWOMAN_DATA_ROOT/wof/fst-street-morphology.bin`.
|
|
15
|
+
* 3. Build-from-dictionaries fallback: `buildStreetMorphologyFST` over core's bundled libpostal
|
|
16
|
+
* `street_types.txt` files — the pre-artifact behavior, kept so a missing artifact degrades to
|
|
17
|
+
* a per-process build rather than a crash.
|
|
18
|
+
*
|
|
19
|
+
* A present-but-unreadable artifact reports through `onWarn` and falls through to the build rung.
|
|
20
|
+
* Browsers can't take the fallback (no fs) — they deserialize the same artifact via
|
|
21
|
+
* `fst-deserialize-web.ts` (see the docs demo loader).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs"
|
|
25
|
+
|
|
26
|
+
import { dataRootPath, resourceDictionaryPath } from "@mailwoman/core/utils"
|
|
27
|
+
|
|
28
|
+
import type { FSTMatcher } from "./fst-matcher.ts"
|
|
29
|
+
import { deserializeFST, readFSTProvenance } from "./fst-serialize.ts"
|
|
30
|
+
import type { FSTProvenance } from "./fst-types.ts"
|
|
31
|
+
import { buildStreetMorphologyFST } from "./street-morphology-fst-builder.ts"
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The sealed artifact's canonical filename — identical in the data-root staging dir and as a weights-package sibling.
|
|
35
|
+
*/
|
|
36
|
+
export const STREET_MORPHOLOGY_ARTIFACT_FILENAME = "fst-street-morphology.bin"
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The staged artifact's default location: `$MAILWOMAN_DATA_ROOT/wof/fst-street-morphology.bin`.
|
|
40
|
+
*/
|
|
41
|
+
export function defaultStreetMorphologyArtifactPath(): string {
|
|
42
|
+
return String(dataRootPath("wof", STREET_MORPHOLOGY_ARTIFACT_FILENAME))
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export interface LoadStreetMorphologyFSTOpts {
|
|
46
|
+
/**
|
|
47
|
+
* Explicit artifact path (e.g. a weights-package sibling). When given it is the ONLY artifact probed — missing or
|
|
48
|
+
* unreadable degrades straight to the dictionary build, never a throw.
|
|
49
|
+
*/
|
|
50
|
+
artifactPath?: string
|
|
51
|
+
/**
|
|
52
|
+
* Dictionaries dir for the build fallback. Defaults to core's bundled libpostal dictionaries.
|
|
53
|
+
*/
|
|
54
|
+
dictionariesDir?: string
|
|
55
|
+
/**
|
|
56
|
+
* Unreadable-artifact diagnostics. Defaults to silent (the caller owns its warn channel).
|
|
57
|
+
*/
|
|
58
|
+
onWarn?: (message: string) => void
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface LoadedStreetMorphologyFST {
|
|
62
|
+
matcher: FSTMatcher
|
|
63
|
+
/**
|
|
64
|
+
* Which rung produced the matcher: a sealed-artifact deserialize, or the per-process dictionary build.
|
|
65
|
+
*/
|
|
66
|
+
source: "artifact" | "built"
|
|
67
|
+
/**
|
|
68
|
+
* The artifact path when `source === "artifact"`.
|
|
69
|
+
*/
|
|
70
|
+
path?: string
|
|
71
|
+
/**
|
|
72
|
+
* Build provenance — read from the artifact trailer, or carried fresh off the fallback build.
|
|
73
|
+
*/
|
|
74
|
+
provenance?: FSTProvenance
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Load the street-morphology matcher: sealed artifact first, per-process dictionary build as the degrade path.
|
|
79
|
+
*/
|
|
80
|
+
export function loadStreetMorphologyFST(opts: LoadStreetMorphologyFSTOpts = {}): LoadedStreetMorphologyFST {
|
|
81
|
+
const warn = opts.onWarn ?? (() => {})
|
|
82
|
+
const artifactPath = opts.artifactPath ?? defaultStreetMorphologyArtifactPath()
|
|
83
|
+
|
|
84
|
+
if (existsSync(artifactPath)) {
|
|
85
|
+
try {
|
|
86
|
+
const buf = readFileSync(artifactPath)
|
|
87
|
+
const matcher = deserializeFST(buf)
|
|
88
|
+
const provenance = readFSTProvenance(buf)
|
|
89
|
+
|
|
90
|
+
return { matcher, source: "artifact", path: artifactPath, ...(provenance ? { provenance } : {}) }
|
|
91
|
+
} catch (error) {
|
|
92
|
+
warn(
|
|
93
|
+
`street-morphology artifact at ${artifactPath} unreadable (${(error as Error).message}) — falling back to the dictionary build`
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const built = buildStreetMorphologyFST({
|
|
99
|
+
dictionariesDir: opts.dictionariesDir ?? resourceDictionaryPath("libpostal"),
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
return { matcher: built.matcher, source: "built", provenance: built.provenance }
|
|
103
|
+
}
|