@mailwoman/resolver-wof-sqlite 10.1.0 → 10.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/README.md +1 -0
- package/lib/address/point/interpolation.ts +6 -2
- package/lib/address/point.ts +15 -14
- package/lib/build/candidate.ts +11 -21
- package/lib/build/slim.ts +1 -1
- package/lib/candidate/importance.ts +1 -1
- package/lib/candidate/lookup.ts +47 -34
- package/lib/candidate/own-name.ts +1 -1
- package/lib/candidate/scoring.ts +29 -29
- package/lib/capitals.ts +3 -3
- package/lib/coincident-roles.ts +15 -4
- package/lib/convention/schema.ts +2 -9
- package/lib/convention.ts +7 -7
- package/lib/coverage-manifest-schema.ts +6 -6
- package/lib/currency-backfill.ts +13 -13
- package/lib/exact-match.ts +1 -1
- package/lib/extracts.ts +1 -1
- package/lib/fst/autocomplete.ts +2 -2
- package/lib/fst/builder.ts +9 -7
- package/lib/fst/deserialize-web.ts +8 -8
- package/lib/fst/freshness.ts +27 -26
- package/lib/fst/matcher.ts +5 -3
- package/lib/fst/serialize.ts +12 -17
- package/lib/fst/types.ts +14 -23
- package/lib/fst.ts +1 -0
- package/lib/fts.ts +1 -1
- package/lib/geonames/aliases.ts +3 -3
- package/lib/geonames/postal.ts +1 -1
- package/lib/index.ts +3 -2
- package/lib/interpolation.ts +7 -6
- package/lib/lookup.ts +26 -20
- package/lib/nsul/lookup.ts +1 -1
- package/lib/place-importance-schema.ts +5 -5
- package/lib/poi/lookup.ts +13 -21
- package/lib/postcode-point-lookup.ts +1 -10
- package/lib/primary-preference.ts +2 -2
- package/lib/proximity-rerank.ts +1 -1
- package/lib/ranking-weights.ts +15 -10
- package/lib/region-keys.ts +2 -2
- package/lib/reverse.ts +45 -18
- package/lib/schema.ts +3 -12
- package/lib/sqlite/convention-source.ts +3 -3
- package/lib/sqlite/utils.ts +1 -1
- package/lib/street/centroid.ts +1 -1
- package/lib/street/morphology/fst/builder.ts +16 -7
- package/lib/street/morphology/fst/loader.ts +8 -7
- package/lib/street/name-lookup.ts +4 -8
- package/lib/street/normalize.ts +3 -3
- package/lib/street/segment-schema.ts +2 -2
- package/lib/types.ts +17 -12
- package/lib/uprn/lookup.ts +1 -1
- package/lib/weights-overlay-linker.ts +38 -26
- package/out/address/point/interpolation.d.ts +1 -0
- package/out/address/point/interpolation.d.ts.map +1 -1
- package/out/address/point/interpolation.js +5 -2
- package/out/address/point/interpolation.js.map +1 -1
- package/out/address/point.d.ts +5 -4
- package/out/address/point.d.ts.map +1 -1
- package/out/address/point.js +11 -10
- package/out/address/point.js.map +1 -1
- package/out/build/candidate.d.ts +7 -17
- package/out/build/candidate.d.ts.map +1 -1
- package/out/build/candidate.js +4 -11
- package/out/build/candidate.js.map +1 -1
- package/out/build/slim.js +1 -1
- package/out/build/slim.js.map +1 -1
- package/out/candidate/importance.js +1 -1
- package/out/candidate/importance.js.map +1 -1
- package/out/candidate/lookup.d.ts +8 -5
- package/out/candidate/lookup.d.ts.map +1 -1
- package/out/candidate/lookup.js +35 -25
- package/out/candidate/lookup.js.map +1 -1
- package/out/candidate/own-name.js +1 -1
- package/out/candidate/own-name.js.map +1 -1
- package/out/candidate/scoring.js +27 -25
- package/out/candidate/scoring.js.map +1 -1
- package/out/capitals.d.ts +1 -1
- package/out/capitals.d.ts.map +1 -1
- package/out/capitals.js.map +1 -1
- package/out/coincident-roles.d.ts +4 -4
- package/out/coincident-roles.d.ts.map +1 -1
- package/out/coincident-roles.js +11 -0
- package/out/coincident-roles.js.map +1 -1
- package/out/convention/schema.d.ts +2 -8
- package/out/convention/schema.d.ts.map +1 -1
- package/out/convention/schema.js.map +1 -1
- package/out/convention.d.ts +4 -4
- package/out/convention.d.ts.map +1 -1
- package/out/convention.js +3 -3
- package/out/convention.js.map +1 -1
- package/out/coverage-manifest-schema.d.ts +1 -1
- package/out/coverage-manifest-schema.d.ts.map +1 -1
- package/out/coverage-manifest-schema.js +5 -5
- package/out/coverage-manifest-schema.js.map +1 -1
- package/out/currency-backfill.d.ts +6 -6
- package/out/currency-backfill.d.ts.map +1 -1
- package/out/currency-backfill.js +7 -7
- package/out/currency-backfill.js.map +1 -1
- package/out/exact-match.js +1 -1
- package/out/exact-match.js.map +1 -1
- package/out/extracts.d.ts +1 -1
- package/out/extracts.d.ts.map +1 -1
- package/out/extracts.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 +1 -1
- package/out/fst/autocomplete.js.map +1 -1
- package/out/fst/builder.d.ts.map +1 -1
- package/out/fst/builder.js +9 -7
- package/out/fst/builder.js.map +1 -1
- package/out/fst/deserialize-web.d.ts +1 -1
- package/out/fst/deserialize-web.d.ts.map +1 -1
- package/out/fst/deserialize-web.js +7 -7
- package/out/fst/deserialize-web.js.map +1 -1
- package/out/fst/freshness.d.ts +9 -9
- package/out/fst/freshness.d.ts.map +1 -1
- package/out/fst/freshness.js +19 -18
- package/out/fst/freshness.js.map +1 -1
- package/out/fst/matcher.d.ts +4 -3
- package/out/fst/matcher.d.ts.map +1 -1
- package/out/fst/matcher.js.map +1 -1
- package/out/fst/serialize.d.ts +1 -5
- package/out/fst/serialize.d.ts.map +1 -1
- package/out/fst/serialize.js +11 -15
- package/out/fst/serialize.js.map +1 -1
- package/out/fst/types.d.ts +14 -22
- package/out/fst/types.d.ts.map +1 -1
- package/out/fst.d.ts +1 -0
- package/out/fst.d.ts.map +1 -1
- package/out/fst.js +1 -0
- package/out/fst.js.map +1 -1
- package/out/fts.js +1 -1
- package/out/fts.js.map +1 -1
- package/out/geonames/aliases.d.ts +1 -1
- package/out/geonames/aliases.d.ts.map +1 -1
- package/out/geonames/aliases.js +2 -2
- package/out/geonames/aliases.js.map +1 -1
- package/out/geonames/postal.js +1 -1
- package/out/geonames/postal.js.map +1 -1
- package/out/index.d.ts +3 -2
- 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 +4 -4
- package/out/interpolation.d.ts.map +1 -1
- package/out/interpolation.js +4 -3
- package/out/interpolation.js.map +1 -1
- package/out/lookup.d.ts +4 -3
- package/out/lookup.d.ts.map +1 -1
- package/out/lookup.js +22 -14
- package/out/lookup.js.map +1 -1
- package/out/nsul/lookup.js.map +1 -1
- package/out/place-importance-schema.d.ts +2 -2
- package/out/place-importance-schema.d.ts.map +1 -1
- package/out/place-importance-schema.js +3 -3
- package/out/place-importance-schema.js.map +1 -1
- package/out/poi/lookup.d.ts +10 -13
- package/out/poi/lookup.d.ts.map +1 -1
- package/out/poi/lookup.js +3 -7
- package/out/poi/lookup.js.map +1 -1
- package/out/postcode-point-lookup.d.ts +1 -9
- package/out/postcode-point-lookup.d.ts.map +1 -1
- package/out/postcode-point-lookup.js.map +1 -1
- package/out/primary-preference.js +2 -2
- package/out/primary-preference.js.map +1 -1
- package/out/proximity-rerank.d.ts +1 -1
- package/out/proximity-rerank.d.ts.map +1 -1
- package/out/ranking-weights.d.ts +15 -10
- package/out/ranking-weights.d.ts.map +1 -1
- package/out/ranking-weights.js.map +1 -1
- package/out/region-keys.d.ts +2 -2
- package/out/region-keys.d.ts.map +1 -1
- package/out/region-keys.js.map +1 -1
- package/out/reverse.d.ts +7 -4
- package/out/reverse.d.ts.map +1 -1
- package/out/reverse.js +38 -13
- package/out/reverse.js.map +1 -1
- package/out/schema.d.ts +2 -11
- package/out/schema.d.ts.map +1 -1
- package/out/sqlite/convention-source.d.ts +1 -1
- package/out/sqlite/convention-source.d.ts.map +1 -1
- package/out/sqlite/convention-source.js +2 -2
- package/out/sqlite/convention-source.js.map +1 -1
- package/out/sqlite/utils.d.ts +1 -1
- package/out/sqlite/utils.d.ts.map +1 -1
- package/out/street/centroid.js +1 -1
- package/out/street/centroid.js.map +1 -1
- package/out/street/morphology/fst/builder.d.ts +3 -3
- package/out/street/morphology/fst/builder.d.ts.map +1 -1
- package/out/street/morphology/fst/builder.js +13 -4
- package/out/street/morphology/fst/builder.js.map +1 -1
- package/out/street/morphology/fst/loader.d.ts +4 -4
- package/out/street/morphology/fst/loader.d.ts.map +1 -1
- package/out/street/morphology/fst/loader.js +4 -3
- package/out/street/morphology/fst/loader.js.map +1 -1
- package/out/street/name-lookup.d.ts +2 -2
- package/out/street/name-lookup.d.ts.map +1 -1
- package/out/street/name-lookup.js +2 -4
- package/out/street/name-lookup.js.map +1 -1
- package/out/street/normalize.js +3 -3
- package/out/street/normalize.js.map +1 -1
- package/out/street/segment-schema.d.ts +2 -2
- package/out/street/segment-schema.js +2 -2
- package/out/types.d.ts +16 -12
- package/out/types.d.ts.map +1 -1
- package/out/uprn/lookup.js +1 -1
- package/out/uprn/lookup.js.map +1 -1
- package/out/weights-overlay-linker.d.ts +9 -6
- package/out/weights-overlay-linker.d.ts.map +1 -1
- package/out/weights-overlay-linker.js +21 -7
- package/out/weights-overlay-linker.js.map +1 -1
- package/package.json +13 -13
package/lib/interpolation.ts
CHANGED
|
@@ -35,11 +35,11 @@ export interface InterpolatedHit {
|
|
|
35
35
|
* True when the matched segment side's parity agrees with the house number
|
|
36
36
|
* (or the side is `mixed`); false is an opposite-side fallback.
|
|
37
37
|
*/
|
|
38
|
-
parityMatched
|
|
38
|
+
parityMatched: boolean | null
|
|
39
39
|
/**
|
|
40
40
|
* `both` sits between two known neighbor numbers; `single` is extrapolated from one side only.
|
|
41
41
|
*/
|
|
42
|
-
bracket
|
|
42
|
+
bracket: "both" | "single" | null
|
|
43
43
|
/**
|
|
44
44
|
* Uncertainty radius in meters: half the matched segment length for `tiger_range`, half the
|
|
45
45
|
* bracket span for `address_point`/`both`, or a larger extrapolation penalty for `single`.
|
|
@@ -140,7 +140,7 @@ export class StreetInterpolator<
|
|
|
140
140
|
| PreparedAll<[postcode: string, street: RouteKey, minNumber: number, maxNumber: number], SegmentRow>
|
|
141
141
|
| undefined
|
|
142
142
|
readonly #byStreet: PreparedAll<[street: RouteKey, minNumber: number, maxNumber: number], SegmentRow> | undefined
|
|
143
|
-
readonly #radiusCalibration: number |
|
|
143
|
+
readonly #radiusCalibration: number | null = null
|
|
144
144
|
|
|
145
145
|
constructor(opts: { dbPath?: string; database?: DatabaseClient<DB> }) {
|
|
146
146
|
using resources = new DisposableStack()
|
|
@@ -172,7 +172,7 @@ export class StreetInterpolator<
|
|
|
172
172
|
|
|
173
173
|
// The conformal radius multiplier ships in the extract's `interp_calibration` table
|
|
174
174
|
// and is read once at open time.
|
|
175
|
-
// Extracts without that table leave it
|
|
175
|
+
// Extracts without that table leave it null.
|
|
176
176
|
if (hasTable(this.#db, "interp_calibration")) {
|
|
177
177
|
const row = this.#db.prepare("SELECT radius_multiplier FROM interp_calibration LIMIT 1").get() as
|
|
178
178
|
| { radius_multiplier: unknown }
|
|
@@ -190,9 +190,9 @@ export class StreetInterpolator<
|
|
|
190
190
|
|
|
191
191
|
/**
|
|
192
192
|
* The artifact's own conformal radius multiplier, read from the extract at construction;
|
|
193
|
-
* `
|
|
193
|
+
* `null` when the extract predates the table or contains no valid row.
|
|
194
194
|
*/
|
|
195
|
-
get radiusCalibration(): number |
|
|
195
|
+
get radiusCalibration(): number | null {
|
|
196
196
|
return this.#radiusCalibration
|
|
197
197
|
}
|
|
198
198
|
|
|
@@ -268,6 +268,7 @@ export class StreetInterpolator<
|
|
|
268
268
|
interpolated: true,
|
|
269
269
|
method: "tiger_range",
|
|
270
270
|
parityMatched,
|
|
271
|
+
bracket: null,
|
|
271
272
|
uncertaintyM: Math.round((lengthKm * 1000) / 2),
|
|
272
273
|
source: best.source,
|
|
273
274
|
release: best.release,
|
package/lib/lookup.ts
CHANGED
|
@@ -65,13 +65,15 @@ export interface WOFSQLitePlaceLookupOpts {
|
|
|
65
65
|
/**
|
|
66
66
|
* When true, build the FTS5 `place_search` virtual table on construction if
|
|
67
67
|
* it is missing, on the main extract only.
|
|
68
|
-
*
|
|
68
|
+
*
|
|
69
|
+
* @defaultValue `false`
|
|
69
70
|
*/
|
|
70
71
|
buildFTS?: boolean
|
|
71
72
|
/**
|
|
72
73
|
* Geographic Rule Engine convention source, either a ready `ConventionSource`
|
|
73
74
|
* or a `{ wofID: Convention }` seed map.
|
|
74
|
-
*
|
|
75
|
+
*
|
|
76
|
+
* @defaultValue `[]`. An empty list resolves every query to `WORLD_DEFAULT`.
|
|
75
77
|
*/
|
|
76
78
|
conventions?: ConventionSource | Record<number, Convention>
|
|
77
79
|
/**
|
|
@@ -307,7 +309,7 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
307
309
|
// The first non-null result wins and unknown strategy names are skipped.
|
|
308
310
|
const convention = this.#conventionFor(query)
|
|
309
311
|
|
|
310
|
-
let
|
|
312
|
+
let found: PlaceCandidate[] = []
|
|
311
313
|
|
|
312
314
|
for (const name of convention.candidateStrategies) {
|
|
313
315
|
const strategy = this.#strategies.get(name)
|
|
@@ -320,14 +322,14 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
320
322
|
|
|
321
323
|
const result = await strategy(query, convention)
|
|
322
324
|
|
|
323
|
-
if (result
|
|
324
|
-
|
|
325
|
+
if (result) {
|
|
326
|
+
found = result
|
|
325
327
|
|
|
326
328
|
break
|
|
327
329
|
}
|
|
328
330
|
}
|
|
329
331
|
|
|
330
|
-
if (
|
|
332
|
+
if (found.length) return found
|
|
331
333
|
|
|
332
334
|
// On a postcode-typed NL-shape miss, retry with the whitespace-joined form then the 4-digit stem,
|
|
333
335
|
// restricted to NL so the same shape elsewhere cannot coarsen to another system's code.
|
|
@@ -350,7 +352,7 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
350
352
|
if (stem !== trimmed) return this.findPlace({ ...query, text: stem })
|
|
351
353
|
}
|
|
352
354
|
|
|
353
|
-
return
|
|
355
|
+
return found
|
|
354
356
|
}
|
|
355
357
|
|
|
356
358
|
/**
|
|
@@ -478,7 +480,7 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
478
480
|
|
|
479
481
|
const extract =
|
|
480
482
|
forceExtract ??
|
|
481
|
-
pickExtractForPlacetype(this.#extracts, firstPlacetype, {
|
|
483
|
+
pickExtractForPlacetype(this.#extracts, firstPlacetype ?? null, {
|
|
482
484
|
country: query.country,
|
|
483
485
|
countriesBySchema: this.#extractCountries,
|
|
484
486
|
})
|
|
@@ -645,7 +647,7 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
645
647
|
: wofAliases
|
|
646
648
|
|
|
647
649
|
const sName = softNameScore(query.text, cand.name, aliases)
|
|
648
|
-
const sPop = populationScaleTerm(cand.population, this.#weights)
|
|
650
|
+
const sPop = populationScaleTerm(cand.population ?? null, this.#weights)
|
|
649
651
|
scored.push({ ...cand, score: w.pc * sPc + w.name * sName + w.pop * sPop, exact: sName >= 1 })
|
|
650
652
|
}
|
|
651
653
|
|
|
@@ -703,22 +705,28 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
703
705
|
)
|
|
704
706
|
|
|
705
707
|
return rows.map((row) => {
|
|
706
|
-
|
|
708
|
+
return {
|
|
707
709
|
id: row.id,
|
|
708
710
|
name: row.name,
|
|
709
711
|
placetype: row.placetype as WOFPlacetype,
|
|
710
712
|
country: row.country ?? "",
|
|
711
713
|
lat: row.lat ?? 0,
|
|
712
714
|
lon: row.lon ?? 0,
|
|
713
|
-
parent_id: row.parent_id ??
|
|
715
|
+
parent_id: row.parent_id ?? null,
|
|
714
716
|
score: 0,
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
717
|
+
population: row.population !== null && row.population > 0 ? row.population : null,
|
|
718
|
+
distanceKm: null,
|
|
719
|
+
exactMatch: null,
|
|
720
|
+
prominence: null,
|
|
721
|
+
referential: null,
|
|
722
|
+
encyclopedic: null,
|
|
723
|
+
importance: null,
|
|
724
|
+
bbox: null,
|
|
725
|
+
mismatch: null,
|
|
726
|
+
containedByQualifier: null,
|
|
727
|
+
regionScopeMiss: null,
|
|
728
|
+
variantAliasExempted: null,
|
|
729
|
+
} satisfies PlaceCandidate
|
|
722
730
|
})
|
|
723
731
|
}
|
|
724
732
|
|
|
@@ -737,5 +745,3 @@ export class WOFSQLitePlaceLookup extends SQLiteLookup<WOFDatabase> implements P
|
|
|
737
745
|
}
|
|
738
746
|
}
|
|
739
747
|
}
|
|
740
|
-
|
|
741
|
-
export type { RankingWeights } from "#ranking-weights"
|
package/lib/nsul/lookup.ts
CHANGED
|
@@ -82,7 +82,7 @@ interface PointRow {
|
|
|
82
82
|
* Disposable, so callers can `using lookup = new NSULLookup(...)`.
|
|
83
83
|
*/
|
|
84
84
|
export class NSULLookup extends SQLiteLookup<NSULDatabase> {
|
|
85
|
-
readonly #postcodeProbe: (uprn: number) => PostcodeRow |
|
|
85
|
+
readonly #postcodeProbe: (uprn: number) => PostcodeRow | null
|
|
86
86
|
readonly #pointsProbe: (pcdsCompact: string) => PointRow[]
|
|
87
87
|
|
|
88
88
|
constructor(opts: NSULLookupOpts) {
|
|
@@ -89,8 +89,8 @@ export const ENCYCLOPEDIC_BOOST_CAP = 0.25
|
|
|
89
89
|
* (`referential` 0), and otherwise the encyclopedic score clamped between the
|
|
90
90
|
* referential score and {@link ENCYCLOPEDIC_BOOST_CAP} above it.
|
|
91
91
|
*/
|
|
92
|
-
export function blendImportance(referential: number, encyclopedic: number | null
|
|
93
|
-
if (encyclopedic === null
|
|
92
|
+
export function blendImportance(referential: number, encyclopedic: number | null): number {
|
|
93
|
+
if (encyclopedic === null) return referential
|
|
94
94
|
|
|
95
95
|
if (referential <= 0) return encyclopedic
|
|
96
96
|
|
|
@@ -159,12 +159,12 @@ export const LEGACY_FALLBACK_EPSILON = 8 * Number.EPSILON
|
|
|
159
159
|
* Bit equality would invent encyclopedic scores.
|
|
160
160
|
*/
|
|
161
161
|
export function splitLegacyImportance(
|
|
162
|
-
legacy: number |
|
|
162
|
+
legacy: number | null,
|
|
163
163
|
population: number | null | undefined
|
|
164
164
|
): { referential: number; encyclopedic?: number } {
|
|
165
|
-
const referential = referentialFromPopulation(population)
|
|
165
|
+
const referential = referentialFromPopulation(population ?? null)
|
|
166
166
|
|
|
167
|
-
if (legacy ===
|
|
167
|
+
if (legacy === null) return { referential }
|
|
168
168
|
|
|
169
169
|
if (Math.abs(legacy - referential) <= LEGACY_FALLBACK_EPSILON * Math.max(referential, 1)) return { referential }
|
|
170
170
|
|
package/lib/poi/lookup.ts
CHANGED
|
@@ -54,23 +54,18 @@ const DEFAULT_LIMIT = 20
|
|
|
54
54
|
|
|
55
55
|
export interface POISearchQuery {
|
|
56
56
|
/**
|
|
57
|
-
* Poi-taxonomy category
|
|
57
|
+
* Poi-taxonomy category ids (string side of the dictionary) to search.
|
|
58
58
|
*
|
|
59
|
-
*
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Fan-out category ids that a single canonical Overture `taxonomy.primary` category
|
|
64
|
-
* rolls up into, as in `supermarket` → `grocery_store`, `organic_grocery_store`, ….
|
|
59
|
+
* A single category is a one-element list.
|
|
60
|
+
* A canonical Overture `taxonomy.primary` category passes every leaf it rolls up into,
|
|
61
|
+
* as in `supermarket` → `grocery_store`, `organic_grocery_store`, ….
|
|
65
62
|
*
|
|
66
|
-
*
|
|
63
|
+
* The k-ring walk probes every resolvable leaf per cell and unions the rows.
|
|
67
64
|
* Unknown leaves are skipped.
|
|
68
65
|
*
|
|
69
|
-
* Supersedes `categoryID` (which is treated as a one-element list `[categoryID]` when this is absent).
|
|
70
|
-
*
|
|
71
66
|
* Ignored when `brandWikidata` is set, since brand wins.
|
|
72
67
|
*/
|
|
73
|
-
categoryIDs?: string[]
|
|
68
|
+
categoryIDs?: readonly string[]
|
|
74
69
|
/**
|
|
75
70
|
* Wikidata QID for brand-exact search.
|
|
76
71
|
*/
|
|
@@ -86,9 +81,11 @@ export interface POISearchQuery {
|
|
|
86
81
|
*/
|
|
87
82
|
center?: { latitude: number; longitude: number }
|
|
88
83
|
/**
|
|
89
|
-
* Ring budget: how many res-9 k-rings to expand before giving up
|
|
84
|
+
* Ring budget: how many res-9 k-rings to expand before giving up.
|
|
90
85
|
*
|
|
91
86
|
* Counts ring 0, so k reaches `maxRings - 1`.
|
|
87
|
+
*
|
|
88
|
+
* @defaultValue `16` ≈ ~5.4 km
|
|
92
89
|
*/
|
|
93
90
|
maxRings?: number
|
|
94
91
|
limit?: number
|
|
@@ -109,7 +106,7 @@ export interface POISearchHit {
|
|
|
109
106
|
* See `POITable.gers_id`.
|
|
110
107
|
*/
|
|
111
108
|
gersID: string | null
|
|
112
|
-
distanceM
|
|
109
|
+
distanceM: number | null
|
|
113
110
|
}
|
|
114
111
|
|
|
115
112
|
/**
|
|
@@ -189,7 +186,7 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
189
186
|
return this.#searchByName(query.name, limit, query.center)
|
|
190
187
|
}
|
|
191
188
|
|
|
192
|
-
if (query.
|
|
189
|
+
if (query.categoryIDs?.length || query.brandWikidata) {
|
|
193
190
|
if (!query.center) {
|
|
194
191
|
throw new Error("POILookup.search: category/brand search requires a `center`")
|
|
195
192
|
}
|
|
@@ -233,12 +230,9 @@ export class POILookup<DB extends POIDatabase = POIDatabase> extends SQLiteLooku
|
|
|
233
230
|
const maxRings = query.maxRings ?? DEFAULT_MAX_RINGS
|
|
234
231
|
const categoryIDs: number[] = []
|
|
235
232
|
|
|
236
|
-
// `categoryIDs` (the fan-out list) supersedes the single `categoryID`.
|
|
237
233
|
// Each id resolves through the dictionary.
|
|
238
234
|
// Unresolved ids are dropped to cover Overture taxonomy drift and identity ids with no rows.
|
|
239
|
-
const
|
|
240
|
-
|
|
241
|
-
for (const id of seedIDs) {
|
|
235
|
+
for (const id of query.categoryIDs ?? []) {
|
|
242
236
|
const resolved = this.#categoryToID.get(id)
|
|
243
237
|
|
|
244
238
|
if (resolved !== undefined) {
|
|
@@ -369,9 +363,7 @@ function toHit(
|
|
|
369
363
|
country: row.country,
|
|
370
364
|
confidence: row.confidence,
|
|
371
365
|
gersID: row.gers_id,
|
|
372
|
-
|
|
373
|
-
? { distanceM: haversineKm(center.latitude, center.longitude, row.latitude, row.longitude) * 1000 }
|
|
374
|
-
: {}),
|
|
366
|
+
distanceM: center ? haversineKm(center.latitude, center.longitude, row.latitude, row.longitude) * 1000 : null,
|
|
375
367
|
}
|
|
376
368
|
}
|
|
377
369
|
|
|
@@ -4,21 +4,12 @@
|
|
|
4
4
|
* @author Teffen Ellis, et al.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
+
import type { PostcodePlace } from "@mailwoman/core/resolver"
|
|
7
8
|
import { DatabaseClient } from "@mailwoman/sqlite/client"
|
|
8
9
|
import type { PathBuilderLike } from "path-ts"
|
|
9
10
|
|
|
10
11
|
import type { WOFDatabase } from "#schema"
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
* One postcode row from the WOF gazetteer, where a `lat`/`lon` of 0 means the
|
|
14
|
-
* postcode is known but has no centroid.
|
|
15
|
-
*/
|
|
16
|
-
export interface PostcodePlace {
|
|
17
|
-
country: string
|
|
18
|
-
lat: number
|
|
19
|
-
lon: number
|
|
20
|
-
}
|
|
21
|
-
|
|
22
13
|
const LOOKUP_SQL =
|
|
23
14
|
"SELECT country, latitude AS lat, longitude AS lon FROM spr WHERE name = ? AND placetype = 'postalcode' AND is_current != 0"
|
|
24
15
|
|
|
@@ -95,7 +95,7 @@ export function rankByPrimaryPreference<R extends PrimaryPreferenceRow>(
|
|
|
95
95
|
placetypes?: ReadonlyMap<number, string>,
|
|
96
96
|
exemptVariantAliases = false
|
|
97
97
|
): Array<RankedRow<R>> {
|
|
98
|
-
let topPrimary: R |
|
|
98
|
+
let topPrimary: R | null = null
|
|
99
99
|
|
|
100
100
|
for (const r of rows) {
|
|
101
101
|
if (r.is_primary === 1 && (topPrimary == null || r.neg_rank < topPrimary.neg_rank)) {
|
|
@@ -126,7 +126,7 @@ export function rankByPrimaryPreference<R extends PrimaryPreferenceRow>(
|
|
|
126
126
|
}
|
|
127
127
|
|
|
128
128
|
const seatPreference = (r: R): number =>
|
|
129
|
-
placetypes
|
|
129
|
+
placetypes &&
|
|
130
130
|
typeof r.placetype_id === "number" &&
|
|
131
131
|
typeof r.population === "number" &&
|
|
132
132
|
r.population > 0 &&
|
package/lib/proximity-rerank.ts
CHANGED
package/lib/ranking-weights.ts
CHANGED
|
@@ -47,17 +47,20 @@ export interface RankingWeights {
|
|
|
47
47
|
* At `proximityScaleKm` it is half magnitude.
|
|
48
48
|
* It decays further as distance increases.
|
|
49
49
|
*
|
|
50
|
-
*
|
|
50
|
+
* Its default is tuned so proximity can overcome a typical FTS rank tie
|
|
51
|
+
* but not dominate a strong text match.
|
|
51
52
|
*/
|
|
52
53
|
proximityBoost: number
|
|
53
54
|
/**
|
|
54
55
|
* Magnitude of the bias-hint term inside the exact-tier prominence sort (the `bias`/viewport path).
|
|
55
56
|
*
|
|
56
|
-
* Deliberately population-scale
|
|
57
|
-
*
|
|
57
|
+
* Deliberately population-scale, so a candidate near the map view
|
|
58
|
+
* or the user beats a distant-but-bigger namesake.
|
|
58
59
|
* "The map view wins" is the feature.
|
|
59
60
|
*
|
|
60
61
|
* Same-region ties, where all candidates are far from every hint, still fall to population.
|
|
62
|
+
*
|
|
63
|
+
* @defaultValue the same value as `populationBoost`
|
|
61
64
|
*/
|
|
62
65
|
biasBoost: number
|
|
63
66
|
/**
|
|
@@ -74,23 +77,22 @@ export interface RankingWeights {
|
|
|
74
77
|
* WOF records population for about 15% of localities (mostly larger ones),
|
|
75
78
|
* and places without it get +0, never a penalty.
|
|
76
79
|
*
|
|
77
|
-
*
|
|
80
|
+
* Its default is tuned so the famous Springfield, IL (pop ~112k) gets about a 0.42 boost,
|
|
78
81
|
* enough to nudge past tiny same-name peers.
|
|
79
82
|
*/
|
|
80
83
|
populationBoost: number
|
|
81
84
|
/**
|
|
82
85
|
* Population (in log10) at which the boost reaches its full magnitude.
|
|
83
86
|
*
|
|
84
|
-
* Default 6, so a population of 1,000,000 gives `populationBoost` exactly.
|
|
85
87
|
* Larger populations cap at the same value (no compounding effect for megacities).
|
|
88
|
+
*
|
|
89
|
+
* @defaultValue `6`, so a population of 1,000,000 gives `populationBoost` exactly
|
|
86
90
|
*/
|
|
87
91
|
populationScaleLog10: number
|
|
88
92
|
/**
|
|
89
93
|
* Tier candidates with an exact name/alias match above candidates that only match
|
|
90
94
|
* partially, before the weighted-sum score is consulted.
|
|
91
95
|
*
|
|
92
|
-
* Default true.
|
|
93
|
-
*
|
|
94
96
|
* The weighted sum adds population as a large additive boost, so famous places
|
|
95
97
|
* surface for unambiguous full-name queries.
|
|
96
98
|
* Population is a prominence prior.
|
|
@@ -103,6 +105,8 @@ export interface RankingWeights {
|
|
|
103
105
|
*
|
|
104
106
|
* Note: tiering re-ranks within the over-fetched candidate window (`limit * 4`),
|
|
105
107
|
* so a pathological exact match outside that window is not rescued.
|
|
108
|
+
*
|
|
109
|
+
* @defaultValue `true`
|
|
106
110
|
*/
|
|
107
111
|
exactMatchTiering: boolean
|
|
108
112
|
/**
|
|
@@ -116,10 +120,11 @@ export interface RankingWeights {
|
|
|
116
120
|
* Fixes unscoped "Åbo" → Turku (its official Swedish name) over a hamlet literally called Åbo.
|
|
117
121
|
* Population still orders within the sub-tier, so Paris → Paris FR is untouched.
|
|
118
122
|
*
|
|
119
|
-
* Default true.
|
|
120
123
|
* It requires a gazetteer carrying the `official` ingest bit.
|
|
121
124
|
*
|
|
122
125
|
* On older DBs without that column, the probe fails soft and behavior matches the flag being off.
|
|
126
|
+
*
|
|
127
|
+
* @defaultValue `true`
|
|
123
128
|
*/
|
|
124
129
|
officialNameExact: boolean
|
|
125
130
|
/**
|
|
@@ -173,7 +178,7 @@ export const DEFAULT_WEIGHTS: RankingWeights = {
|
|
|
173
178
|
* The coordinate-first locality path consumes this fraction directly. {@link populationBoostTerm} scales it.
|
|
174
179
|
*/
|
|
175
180
|
export function populationScaleTerm(
|
|
176
|
-
population: number | null
|
|
181
|
+
population: number | null,
|
|
177
182
|
weights: Pick<RankingWeights, "populationScaleLog10">
|
|
178
183
|
): number {
|
|
179
184
|
if (population == null || population <= 0 || weights.populationScaleLog10 <= 0) return 0
|
|
@@ -189,7 +194,7 @@ export function populationScaleTerm(
|
|
|
189
194
|
* The one formula behind the Node weighted sum and the wasm re-rank, so the two backends cannot drift.
|
|
190
195
|
*/
|
|
191
196
|
export function populationBoostTerm(
|
|
192
|
-
population: number | null
|
|
197
|
+
population: number | null,
|
|
193
198
|
weights: Pick<RankingWeights, "populationBoost" | "populationScaleLog10">
|
|
194
199
|
): number {
|
|
195
200
|
return weights.populationBoost * populationScaleTerm(population, weights)
|
package/lib/region-keys.ts
CHANGED
|
@@ -107,7 +107,7 @@ function withoutSuffix(value: string, suffixes: readonly string[]): string {
|
|
|
107
107
|
* Every expansion lands the canonical name and code folds in the set, so `IL`
|
|
108
108
|
* and `Illinois`, or `WA` and `Western Australia`, meet from either side.
|
|
109
109
|
*/
|
|
110
|
-
export function regionKeys(value: string, countryAlpha2?: string): Set<string> {
|
|
110
|
+
export function regionKeys(value: string, countryAlpha2?: string | null): Set<string> {
|
|
111
111
|
const keys = new Set([normalizeLocalityForKey(value)])
|
|
112
112
|
|
|
113
113
|
for (const stripped of [
|
|
@@ -157,7 +157,7 @@ export function regionKeys(value: string, countryAlpha2?: string): Set<string> {
|
|
|
157
157
|
* The board has no case for this stored form.
|
|
158
158
|
* Add one before building a change for this suffix.
|
|
159
159
|
*/
|
|
160
|
-
export function regionQualifierProbeKeys(value: string, countryAlpha2?: string): Set<string> {
|
|
160
|
+
export function regionQualifierProbeKeys(value: string, countryAlpha2?: string | null): Set<string> {
|
|
161
161
|
const keys = regionKeys(value, countryAlpha2)
|
|
162
162
|
// Snapshot before widening: the loop adds `county <key>` members that must not themselves be revisited.
|
|
163
163
|
const bare = [...keys]
|
package/lib/reverse.ts
CHANGED
|
@@ -103,19 +103,21 @@ export interface ReverseGeocodeOpts {
|
|
|
103
103
|
/**
|
|
104
104
|
* Restrict the hierarchy to these placetypes (both the bbox candidates and the descent tiers).
|
|
105
105
|
*
|
|
106
|
-
* Default: every admin placetype in the gazetteer.
|
|
107
106
|
* E.g.
|
|
108
107
|
*
|
|
109
108
|
* `["region", "county", "locality"]` to skip the neighborhood grain.
|
|
109
|
+
*
|
|
110
|
+
* @defaultValue every admin placetype in the gazetteer.
|
|
110
111
|
*/
|
|
111
112
|
placetypes?: WOFPlacetype[]
|
|
112
113
|
/**
|
|
113
114
|
* Cap on the bbox candidate fetch.
|
|
114
115
|
*
|
|
115
|
-
* The default is 128.
|
|
116
116
|
* Current measurements show that this covers a dense metro.
|
|
117
117
|
*
|
|
118
118
|
* The most bbox-overlapping point we have measured is a few dozen neighborhoods plus the admin chain.
|
|
119
|
+
*
|
|
120
|
+
* @defaultValue `128`
|
|
119
121
|
*/
|
|
120
122
|
maxCandidates?: number
|
|
121
123
|
/**
|
|
@@ -124,7 +126,8 @@ export interface ReverseGeocodeOpts {
|
|
|
124
126
|
* This keeps a sparse gazetteer from "refining" to a far-away sibling.
|
|
125
127
|
*
|
|
126
128
|
* Polygon-confirmed steps ignore it (containment is exact regardless of centroid distance).
|
|
127
|
-
*
|
|
129
|
+
*
|
|
130
|
+
* @defaultValue `25 km`
|
|
128
131
|
*/
|
|
129
132
|
maxApproximateKm?: number
|
|
130
133
|
}
|
|
@@ -160,23 +163,35 @@ interface CandidateRow {
|
|
|
160
163
|
lon: number
|
|
161
164
|
}
|
|
162
165
|
|
|
163
|
-
|
|
164
|
-
|
|
166
|
+
const ABSENT_CANDIDATE_FIELDS = {
|
|
167
|
+
parent_id: null,
|
|
168
|
+
distanceKm: null,
|
|
169
|
+
exactMatch: null,
|
|
170
|
+
prominence: null,
|
|
171
|
+
population: null,
|
|
172
|
+
referential: null,
|
|
173
|
+
encyclopedic: null,
|
|
174
|
+
importance: null,
|
|
175
|
+
bbox: null,
|
|
176
|
+
mismatch: null,
|
|
177
|
+
containedByQualifier: null,
|
|
178
|
+
regionScopeMiss: null,
|
|
179
|
+
variantAliasExempted: null,
|
|
180
|
+
} as const satisfies Partial<PlaceCandidate>
|
|
181
|
+
|
|
182
|
+
function toPlaceCandidate(row: CandidateRow, distanceKm: number | null = null): PlaceCandidate {
|
|
183
|
+
return {
|
|
184
|
+
...ABSENT_CANDIDATE_FIELDS,
|
|
165
185
|
id: row.id,
|
|
166
186
|
name: row.name,
|
|
167
187
|
placetype: row.placetype as WOFPlacetype,
|
|
168
188
|
country: row.country ?? "",
|
|
169
189
|
lat: row.lat,
|
|
170
190
|
lon: row.lon,
|
|
171
|
-
parent_id: row.parent_id ??
|
|
191
|
+
parent_id: row.parent_id ?? null,
|
|
172
192
|
score: 0,
|
|
193
|
+
distanceKm,
|
|
173
194
|
}
|
|
174
|
-
|
|
175
|
-
if (distanceKm !== undefined) {
|
|
176
|
-
c.distanceKm = distanceKm
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
return c
|
|
180
195
|
}
|
|
181
196
|
|
|
182
197
|
export class WOFReverseGeocoder implements Disposable {
|
|
@@ -253,7 +268,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
253
268
|
* The async method above exists only for call-site symmetry with `PlaceLookup.findPlace`.
|
|
254
269
|
*
|
|
255
270
|
* Exposed directly for callers that can't await mid-call
|
|
256
|
-
* (e.g. `mailwoman/poi-executor.ts`'s `createPOIExecutor`, whose `
|
|
271
|
+
* (e.g. `mailwoman/poi-executor.ts`'s `createPOIExecutor`, whose `POIQueryResult`
|
|
257
272
|
* return type is synchronous by interface. See `poi-intent.ts`'s `deps.execute`).
|
|
258
273
|
*/
|
|
259
274
|
reverseGeocodeSync(lat: number, lon: number, opts: ReverseGeocodeOpts = {}): ReverseGeocodeResult {
|
|
@@ -310,7 +325,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
310
325
|
// Approximate descent into finer tiers than the winner.
|
|
311
326
|
let current = winner
|
|
312
327
|
let currentConfirmed = winnerConfirmed
|
|
313
|
-
let currentDistanceKm = currentConfirmed ?
|
|
328
|
+
let currentDistanceKm = currentConfirmed ? null : haversineKm(lat, lon, current.lat, current.lon)
|
|
314
329
|
|
|
315
330
|
for (const tier of DESCENT_TIERS) {
|
|
316
331
|
if (placetypeDepth(tier) <= placetypeDepth(current.placetype)) continue
|
|
@@ -319,7 +334,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
319
334
|
const kids = this.#descendants(current.id, tier, lat, lon, maxApproximateKm)
|
|
320
335
|
let next: CandidateRow | null = null
|
|
321
336
|
let nextConfirmed = false
|
|
322
|
-
let nextKm: number |
|
|
337
|
+
let nextKm: number | null = null
|
|
323
338
|
let bestKm = Infinity
|
|
324
339
|
|
|
325
340
|
for (const k of kids) {
|
|
@@ -328,7 +343,7 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
328
343
|
if (contains === true) {
|
|
329
344
|
next = k
|
|
330
345
|
nextConfirmed = true
|
|
331
|
-
nextKm =
|
|
346
|
+
nextKm = null
|
|
332
347
|
|
|
333
348
|
break
|
|
334
349
|
}
|
|
@@ -360,7 +375,13 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
360
375
|
|
|
361
376
|
for (const a of ancestorLineage(this.#admin, current.id)) {
|
|
362
377
|
if (!byID.has(a.id)) {
|
|
363
|
-
byID.set(a.id, {
|
|
378
|
+
byID.set(a.id, {
|
|
379
|
+
...ABSENT_CANDIDATE_FIELDS,
|
|
380
|
+
...a,
|
|
381
|
+
placetype: a.placetype as WOFPlacetype,
|
|
382
|
+
country: a.country ?? "",
|
|
383
|
+
score: 0,
|
|
384
|
+
})
|
|
364
385
|
}
|
|
365
386
|
}
|
|
366
387
|
|
|
@@ -369,7 +390,13 @@ export class WOFReverseGeocoder implements Disposable {
|
|
|
369
390
|
|
|
370
391
|
for (const a of ancestorLineage(this.#admin, winner.id)) {
|
|
371
392
|
if (!byID.has(a.id)) {
|
|
372
|
-
byID.set(a.id, {
|
|
393
|
+
byID.set(a.id, {
|
|
394
|
+
...ABSENT_CANDIDATE_FIELDS,
|
|
395
|
+
...a,
|
|
396
|
+
placetype: a.placetype as WOFPlacetype,
|
|
397
|
+
country: a.country ?? "",
|
|
398
|
+
score: 0,
|
|
399
|
+
})
|
|
373
400
|
}
|
|
374
401
|
}
|
|
375
402
|
}
|
package/lib/schema.ts
CHANGED
|
@@ -14,6 +14,8 @@
|
|
|
14
14
|
* - Per-table sources under https://github.com/whosonfirst/go-whosonfirst-sqlite-features
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
|
+
import type { KeyValueMetaTable } from "@mailwoman/sqlite/database-schema"
|
|
18
|
+
|
|
17
19
|
/**
|
|
18
20
|
* The FTS5 virtual table this package builds on first open.
|
|
19
21
|
* It is local to this repository.
|
|
@@ -193,19 +195,8 @@ export interface CoincidentRolesTable {
|
|
|
193
195
|
*
|
|
194
196
|
* The build and augment writers adopt it so a column rename is a compile error on both sides.
|
|
195
197
|
*/
|
|
196
|
-
/**
|
|
197
|
-
* The provenance row every built extract includes: source fingerprints stored with
|
|
198
|
-
* the database rather than in a document that can drift from it.
|
|
199
|
-
*
|
|
200
|
-
* Written by the postcode builders in `mailwoman/gazetteer-pipeline`.
|
|
201
|
-
*/
|
|
202
|
-
export interface ExtractMetaTable {
|
|
203
|
-
key: string
|
|
204
|
-
value: string | null
|
|
205
|
-
}
|
|
206
|
-
|
|
207
198
|
export interface WOFDatabase {
|
|
208
|
-
meta:
|
|
199
|
+
meta: KeyValueMetaTable
|
|
209
200
|
place_search: PlaceSearchTable
|
|
210
201
|
spr: SprTable
|
|
211
202
|
names: NamesTable
|
|
@@ -32,10 +32,10 @@ export class SqliteConventionSource<DB> implements ConventionSource {
|
|
|
32
32
|
this.#schema = schema
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
get(wofID: number): Convention |
|
|
35
|
+
get(wofID: number): Convention | null {
|
|
36
36
|
const cached = this.#cache.get(wofID)
|
|
37
37
|
|
|
38
|
-
if (cached !== undefined) return cached
|
|
38
|
+
if (cached !== undefined) return cached
|
|
39
39
|
let value: Convention | null = null
|
|
40
40
|
|
|
41
41
|
try {
|
|
@@ -52,6 +52,6 @@ export class SqliteConventionSource<DB> implements ConventionSource {
|
|
|
52
52
|
|
|
53
53
|
this.#cache.set(wofID, value)
|
|
54
54
|
|
|
55
|
-
return value
|
|
55
|
+
return value
|
|
56
56
|
}
|
|
57
57
|
}
|
package/lib/sqlite/utils.ts
CHANGED
|
@@ -14,7 +14,7 @@ import { hasColumn as columnExists, tableExists } from "@mailwoman/sqlite/intros
|
|
|
14
14
|
*
|
|
15
15
|
* `StatementSync` accepts any `SQLInputValue[]`, which would erase tagged key types at the call site.
|
|
16
16
|
*/
|
|
17
|
-
export type PreparedGet<Parameters extends SQLInputValue[], Row> = (...parameters: Parameters) => Row |
|
|
17
|
+
export type PreparedGet<Parameters extends SQLInputValue[], Row> = (...parameters: Parameters) => Row | null
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* Prepares a single-row query with a typed parameter tuple.
|
package/lib/street/centroid.ts
CHANGED
|
@@ -92,7 +92,7 @@ export class StreetCentroidSqliteLookup implements StreetCentroidLookup {
|
|
|
92
92
|
|
|
93
93
|
if (!streetNorm) return null
|
|
94
94
|
|
|
95
|
-
let row: AggRow |
|
|
95
|
+
let row: AggRow | null = null
|
|
96
96
|
|
|
97
97
|
if (query.postcode?.trim()) {
|
|
98
98
|
row = this.#byPostcode(query.postcode.trim(), streetNorm)
|