@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.
Files changed (212) hide show
  1. package/README.md +1 -0
  2. package/lib/address/point/interpolation.ts +6 -2
  3. package/lib/address/point.ts +15 -14
  4. package/lib/build/candidate.ts +11 -21
  5. package/lib/build/slim.ts +1 -1
  6. package/lib/candidate/importance.ts +1 -1
  7. package/lib/candidate/lookup.ts +47 -34
  8. package/lib/candidate/own-name.ts +1 -1
  9. package/lib/candidate/scoring.ts +29 -29
  10. package/lib/capitals.ts +3 -3
  11. package/lib/coincident-roles.ts +15 -4
  12. package/lib/convention/schema.ts +2 -9
  13. package/lib/convention.ts +7 -7
  14. package/lib/coverage-manifest-schema.ts +6 -6
  15. package/lib/currency-backfill.ts +13 -13
  16. package/lib/exact-match.ts +1 -1
  17. package/lib/extracts.ts +1 -1
  18. package/lib/fst/autocomplete.ts +2 -2
  19. package/lib/fst/builder.ts +9 -7
  20. package/lib/fst/deserialize-web.ts +8 -8
  21. package/lib/fst/freshness.ts +27 -26
  22. package/lib/fst/matcher.ts +5 -3
  23. package/lib/fst/serialize.ts +12 -17
  24. package/lib/fst/types.ts +14 -23
  25. package/lib/fst.ts +1 -0
  26. package/lib/fts.ts +1 -1
  27. package/lib/geonames/aliases.ts +3 -3
  28. package/lib/geonames/postal.ts +1 -1
  29. package/lib/index.ts +3 -2
  30. package/lib/interpolation.ts +7 -6
  31. package/lib/lookup.ts +26 -20
  32. package/lib/nsul/lookup.ts +1 -1
  33. package/lib/place-importance-schema.ts +5 -5
  34. package/lib/poi/lookup.ts +13 -21
  35. package/lib/postcode-point-lookup.ts +1 -10
  36. package/lib/primary-preference.ts +2 -2
  37. package/lib/proximity-rerank.ts +1 -1
  38. package/lib/ranking-weights.ts +15 -10
  39. package/lib/region-keys.ts +2 -2
  40. package/lib/reverse.ts +45 -18
  41. package/lib/schema.ts +3 -12
  42. package/lib/sqlite/convention-source.ts +3 -3
  43. package/lib/sqlite/utils.ts +1 -1
  44. package/lib/street/centroid.ts +1 -1
  45. package/lib/street/morphology/fst/builder.ts +16 -7
  46. package/lib/street/morphology/fst/loader.ts +8 -7
  47. package/lib/street/name-lookup.ts +4 -8
  48. package/lib/street/normalize.ts +3 -3
  49. package/lib/street/segment-schema.ts +2 -2
  50. package/lib/types.ts +17 -12
  51. package/lib/uprn/lookup.ts +1 -1
  52. package/lib/weights-overlay-linker.ts +38 -26
  53. package/out/address/point/interpolation.d.ts +1 -0
  54. package/out/address/point/interpolation.d.ts.map +1 -1
  55. package/out/address/point/interpolation.js +5 -2
  56. package/out/address/point/interpolation.js.map +1 -1
  57. package/out/address/point.d.ts +5 -4
  58. package/out/address/point.d.ts.map +1 -1
  59. package/out/address/point.js +11 -10
  60. package/out/address/point.js.map +1 -1
  61. package/out/build/candidate.d.ts +7 -17
  62. package/out/build/candidate.d.ts.map +1 -1
  63. package/out/build/candidate.js +4 -11
  64. package/out/build/candidate.js.map +1 -1
  65. package/out/build/slim.js +1 -1
  66. package/out/build/slim.js.map +1 -1
  67. package/out/candidate/importance.js +1 -1
  68. package/out/candidate/importance.js.map +1 -1
  69. package/out/candidate/lookup.d.ts +8 -5
  70. package/out/candidate/lookup.d.ts.map +1 -1
  71. package/out/candidate/lookup.js +35 -25
  72. package/out/candidate/lookup.js.map +1 -1
  73. package/out/candidate/own-name.js +1 -1
  74. package/out/candidate/own-name.js.map +1 -1
  75. package/out/candidate/scoring.js +27 -25
  76. package/out/candidate/scoring.js.map +1 -1
  77. package/out/capitals.d.ts +1 -1
  78. package/out/capitals.d.ts.map +1 -1
  79. package/out/capitals.js.map +1 -1
  80. package/out/coincident-roles.d.ts +4 -4
  81. package/out/coincident-roles.d.ts.map +1 -1
  82. package/out/coincident-roles.js +11 -0
  83. package/out/coincident-roles.js.map +1 -1
  84. package/out/convention/schema.d.ts +2 -8
  85. package/out/convention/schema.d.ts.map +1 -1
  86. package/out/convention/schema.js.map +1 -1
  87. package/out/convention.d.ts +4 -4
  88. package/out/convention.d.ts.map +1 -1
  89. package/out/convention.js +3 -3
  90. package/out/convention.js.map +1 -1
  91. package/out/coverage-manifest-schema.d.ts +1 -1
  92. package/out/coverage-manifest-schema.d.ts.map +1 -1
  93. package/out/coverage-manifest-schema.js +5 -5
  94. package/out/coverage-manifest-schema.js.map +1 -1
  95. package/out/currency-backfill.d.ts +6 -6
  96. package/out/currency-backfill.d.ts.map +1 -1
  97. package/out/currency-backfill.js +7 -7
  98. package/out/currency-backfill.js.map +1 -1
  99. package/out/exact-match.js +1 -1
  100. package/out/exact-match.js.map +1 -1
  101. package/out/extracts.d.ts +1 -1
  102. package/out/extracts.d.ts.map +1 -1
  103. package/out/extracts.js.map +1 -1
  104. package/out/fst/autocomplete.d.ts +1 -1
  105. package/out/fst/autocomplete.d.ts.map +1 -1
  106. package/out/fst/autocomplete.js +1 -1
  107. package/out/fst/autocomplete.js.map +1 -1
  108. package/out/fst/builder.d.ts.map +1 -1
  109. package/out/fst/builder.js +9 -7
  110. package/out/fst/builder.js.map +1 -1
  111. package/out/fst/deserialize-web.d.ts +1 -1
  112. package/out/fst/deserialize-web.d.ts.map +1 -1
  113. package/out/fst/deserialize-web.js +7 -7
  114. package/out/fst/deserialize-web.js.map +1 -1
  115. package/out/fst/freshness.d.ts +9 -9
  116. package/out/fst/freshness.d.ts.map +1 -1
  117. package/out/fst/freshness.js +19 -18
  118. package/out/fst/freshness.js.map +1 -1
  119. package/out/fst/matcher.d.ts +4 -3
  120. package/out/fst/matcher.d.ts.map +1 -1
  121. package/out/fst/matcher.js.map +1 -1
  122. package/out/fst/serialize.d.ts +1 -5
  123. package/out/fst/serialize.d.ts.map +1 -1
  124. package/out/fst/serialize.js +11 -15
  125. package/out/fst/serialize.js.map +1 -1
  126. package/out/fst/types.d.ts +14 -22
  127. package/out/fst/types.d.ts.map +1 -1
  128. package/out/fst.d.ts +1 -0
  129. package/out/fst.d.ts.map +1 -1
  130. package/out/fst.js +1 -0
  131. package/out/fst.js.map +1 -1
  132. package/out/fts.js +1 -1
  133. package/out/fts.js.map +1 -1
  134. package/out/geonames/aliases.d.ts +1 -1
  135. package/out/geonames/aliases.d.ts.map +1 -1
  136. package/out/geonames/aliases.js +2 -2
  137. package/out/geonames/aliases.js.map +1 -1
  138. package/out/geonames/postal.js +1 -1
  139. package/out/geonames/postal.js.map +1 -1
  140. package/out/index.d.ts +3 -2
  141. package/out/index.d.ts.map +1 -1
  142. package/out/index.js +1 -0
  143. package/out/index.js.map +1 -1
  144. package/out/interpolation.d.ts +4 -4
  145. package/out/interpolation.d.ts.map +1 -1
  146. package/out/interpolation.js +4 -3
  147. package/out/interpolation.js.map +1 -1
  148. package/out/lookup.d.ts +4 -3
  149. package/out/lookup.d.ts.map +1 -1
  150. package/out/lookup.js +22 -14
  151. package/out/lookup.js.map +1 -1
  152. package/out/nsul/lookup.js.map +1 -1
  153. package/out/place-importance-schema.d.ts +2 -2
  154. package/out/place-importance-schema.d.ts.map +1 -1
  155. package/out/place-importance-schema.js +3 -3
  156. package/out/place-importance-schema.js.map +1 -1
  157. package/out/poi/lookup.d.ts +10 -13
  158. package/out/poi/lookup.d.ts.map +1 -1
  159. package/out/poi/lookup.js +3 -7
  160. package/out/poi/lookup.js.map +1 -1
  161. package/out/postcode-point-lookup.d.ts +1 -9
  162. package/out/postcode-point-lookup.d.ts.map +1 -1
  163. package/out/postcode-point-lookup.js.map +1 -1
  164. package/out/primary-preference.js +2 -2
  165. package/out/primary-preference.js.map +1 -1
  166. package/out/proximity-rerank.d.ts +1 -1
  167. package/out/proximity-rerank.d.ts.map +1 -1
  168. package/out/ranking-weights.d.ts +15 -10
  169. package/out/ranking-weights.d.ts.map +1 -1
  170. package/out/ranking-weights.js.map +1 -1
  171. package/out/region-keys.d.ts +2 -2
  172. package/out/region-keys.d.ts.map +1 -1
  173. package/out/region-keys.js.map +1 -1
  174. package/out/reverse.d.ts +7 -4
  175. package/out/reverse.d.ts.map +1 -1
  176. package/out/reverse.js +38 -13
  177. package/out/reverse.js.map +1 -1
  178. package/out/schema.d.ts +2 -11
  179. package/out/schema.d.ts.map +1 -1
  180. package/out/sqlite/convention-source.d.ts +1 -1
  181. package/out/sqlite/convention-source.d.ts.map +1 -1
  182. package/out/sqlite/convention-source.js +2 -2
  183. package/out/sqlite/convention-source.js.map +1 -1
  184. package/out/sqlite/utils.d.ts +1 -1
  185. package/out/sqlite/utils.d.ts.map +1 -1
  186. package/out/street/centroid.js +1 -1
  187. package/out/street/centroid.js.map +1 -1
  188. package/out/street/morphology/fst/builder.d.ts +3 -3
  189. package/out/street/morphology/fst/builder.d.ts.map +1 -1
  190. package/out/street/morphology/fst/builder.js +13 -4
  191. package/out/street/morphology/fst/builder.js.map +1 -1
  192. package/out/street/morphology/fst/loader.d.ts +4 -4
  193. package/out/street/morphology/fst/loader.d.ts.map +1 -1
  194. package/out/street/morphology/fst/loader.js +4 -3
  195. package/out/street/morphology/fst/loader.js.map +1 -1
  196. package/out/street/name-lookup.d.ts +2 -2
  197. package/out/street/name-lookup.d.ts.map +1 -1
  198. package/out/street/name-lookup.js +2 -4
  199. package/out/street/name-lookup.js.map +1 -1
  200. package/out/street/normalize.js +3 -3
  201. package/out/street/normalize.js.map +1 -1
  202. package/out/street/segment-schema.d.ts +2 -2
  203. package/out/street/segment-schema.js +2 -2
  204. package/out/types.d.ts +16 -12
  205. package/out/types.d.ts.map +1 -1
  206. package/out/uprn/lookup.js +1 -1
  207. package/out/uprn/lookup.js.map +1 -1
  208. package/out/weights-overlay-linker.d.ts +9 -6
  209. package/out/weights-overlay-linker.d.ts.map +1 -1
  210. package/out/weights-overlay-linker.js +21 -7
  211. package/out/weights-overlay-linker.js.map +1 -1
  212. package/package.json +13 -13
@@ -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?: boolean
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?: "both" | "single"
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 | undefined
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 undefined.
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
- * `undefined` when the extract predates the table or contains no valid row.
193
+ * `null` when the extract predates the table or contains no valid row.
194
194
  */
195
- get radiusCalibration(): number | undefined {
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
- * Default false.
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
- * Default empty resolves every query to `WORLD_DEFAULT`.
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 outcome: PlaceCandidate[] = []
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 !== null) {
324
- outcome = result
325
+ if (result) {
326
+ found = result
325
327
 
326
328
  break
327
329
  }
328
330
  }
329
331
 
330
- if (outcome.length) return outcome
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 outcome
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
- const c: PlaceCandidate = {
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 ?? undefined,
715
+ parent_id: row.parent_id ?? null,
714
716
  score: 0,
715
- }
716
-
717
- if (row.population !== null && row.population > 0) {
718
- c.population = row.population
719
- }
720
-
721
- return c
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"
@@ -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 | undefined
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 | undefined): number {
93
- if (encyclopedic === null || encyclopedic === undefined) return referential
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 | undefined,
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 === undefined) return { referential }
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 id (string side of the dictionary).
57
+ * Poi-taxonomy category ids (string side of the dictionary) to search.
58
58
  *
59
- * Ignored when `brandWikidata` is also set, since brand wins.
60
- */
61
- categoryID?: string
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
- * When set, the k-ring walk probes every resolvable leaf per cell and unions the rows.
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 (default 16 ≈ ~5.4 km).
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?: number
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.categoryID || (query.categoryIDs && query.categoryIDs.length) || query.brandWikidata) {
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 seedIDs = query.categoryIDs?.length ? query.categoryIDs : query.categoryID ? [query.categoryID] : []
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
- ...(center
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 | undefined
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 != null &&
129
+ placetypes &&
130
130
  typeof r.placetype_id === "number" &&
131
131
  typeof r.population === "number" &&
132
132
  r.population > 0 &&
@@ -74,7 +74,7 @@ export interface ProximityRerankable {
74
74
  lat: number
75
75
  lon: number
76
76
  score: number
77
- prominence?: number
77
+ prominence?: number | null
78
78
  }
79
79
 
80
80
  /**
@@ -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
- * Default tuned so proximity can overcome a typical FTS rank tie but not dominate a strong text match.
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 (default = populationBoost), so a candidate near
57
- * the map view or the user beats a distant-but-bigger namesake.
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
- * Default tuned so the famous Springfield, IL (pop ~112k) gets about a 0.42 boost,
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 | undefined,
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 | undefined,
197
+ population: number | null,
193
198
  weights: Pick<RankingWeights, "populationBoost" | "populationScaleLog10">
194
199
  ): number {
195
200
  return weights.populationBoost * populationScaleTerm(population, weights)
@@ -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
- * Default 25 km.
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
- function toPlaceCandidate(row: CandidateRow, distanceKm?: number): PlaceCandidate {
164
- const c: PlaceCandidate = {
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 ?? undefined,
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 `POIIntentOutcome`
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 ? undefined : haversineKm(lat, lon, current.lat, current.lon)
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 | undefined
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 = undefined
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, { ...a, placetype: a.placetype as WOFPlacetype, country: a.country ?? "", score: 0 })
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, { ...a, placetype: a.placetype as WOFPlacetype, country: a.country ?? "", score: 0 })
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: ExtractMetaTable
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 | undefined {
35
+ get(wofID: number): Convention | null {
36
36
  const cached = this.#cache.get(wofID)
37
37
 
38
- if (cached !== undefined) return cached ?? undefined
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 ?? undefined
55
+ return value
56
56
  }
57
57
  }
@@ -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 | undefined
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.
@@ -92,7 +92,7 @@ export class StreetCentroidSqliteLookup implements StreetCentroidLookup {
92
92
 
93
93
  if (!streetNorm) return null
94
94
 
95
- let row: AggRow | undefined
95
+ let row: AggRow | null = null
96
96
 
97
97
  if (query.postcode?.trim()) {
98
98
  row = this.#byPostcode(query.postcode.trim(), streetNorm)