@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
package/README.md CHANGED
@@ -166,6 +166,7 @@ You can also build the index programmatically via the package's `./fts` subpath:
166
166
 
167
167
  ```ts
168
168
  import { DatabaseSync } from "node:sqlite"
169
+
169
170
  import { buildPlaceSearchFts } from "@mailwoman/resolver-wof-sqlite/fts"
170
171
 
171
172
  using db = new DatabaseSync("/path/to/wof.db")
@@ -107,6 +107,8 @@ export class AddressPointInterpolator<
107
107
  this.#resources = resources.move()
108
108
  }
109
109
 
110
+ readonly radiusCalibration = null
111
+
110
112
  find(query: InterpolationQuery): InterpolatedHit | null {
111
113
  const numberRaw = query.number.trim()
112
114
 
@@ -168,8 +170,8 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
168
170
 
169
171
  // Nearest known number below and above the query.
170
172
  // The rows never contain n itself.
171
- let below: NumberAnchor | undefined
172
- let above: NumberAnchor | undefined
173
+ let below: NumberAnchor | null = null
174
+ let above: NumberAnchor | null = null
173
175
 
174
176
  for (const anchor of anchors) {
175
177
  if (anchor.n < n) {
@@ -190,6 +192,7 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
190
192
  lon: below.lon + (above.lon - below.lon) * t,
191
193
  interpolated: true,
192
194
  method: "address_point",
195
+ parityMatched: null,
193
196
  bracket: "both",
194
197
  uncertaintyM: Math.round(spanM / 2),
195
198
  source: below.source,
@@ -217,6 +220,7 @@ function interpolateFromNeighbors(rows: readonly PointRow[], n: number): Interpo
217
220
  lon,
218
221
  interpolated: true,
219
222
  method: "address_point",
223
+ parityMatched: null,
220
224
  bracket: "single",
221
225
  // Explicitly larger than the both-sided radius: the whole pair span plus the overshoot.
222
226
  uncertaintyM: Math.round(pairM + overshootM),
@@ -76,13 +76,14 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
76
76
  * @param dbPath Extract path.
77
77
  * @param opts.streetLocale The street-normalization locale the extract was built with.
78
78
  * A mismatch makes every key miss.
79
- * It defaults to `"us"`.
80
79
  * @param opts.localityKeys Whether the extract's `locality_norm` holds full place names.
81
80
  * With `"abbreviated"`, a row's locality can select a row but never rejects one.
82
81
  * The US situs extract uses NAD city fields.
83
82
  * Some counties abbreviate them (`addi` for Addison).
84
83
  * Other counties replace them with the parent town.
85
- * It defaults to `"abbreviated"` for `"us"` and `"full"` for every other locale.
84
+ *
85
+ * @defaultValue `"us"`
86
+ * @defaultValue `"abbreviated"` for `"us"` and `"full"` for every other locale.
86
87
  */
87
88
  constructor(
88
89
  dbPath: PathBuilderLike,
@@ -133,7 +134,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
133
134
  locality?: string
134
135
  region?: string
135
136
  subregion?: string
136
- bbox?: { minLat: number; maxLat: number; minLon: number; maxLon: number }
137
+ bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number } | null
137
138
  }): AddressPointHit | null {
138
139
  if (!this.#byPostcode || !this.#byLocality || !this.#byBbox) return null
139
140
  const number = normalizeHouseNumberForKey(query.number, this.#locale)
@@ -154,7 +155,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
154
155
  // `streetKeyVariants` yields the literal key first.
155
156
  // Each variant runs the full number fallback sequence.
156
157
  // The first variant with a match wins.
157
- let row: AddressPointRow | undefined
158
+ let row: AddressPointRow | null = null
158
159
 
159
160
  for (const streetNorm of streetKeyVariants(query.street, streetLocaleForSurface(query.street, this.#locale))) {
160
161
  row = this.#findForKey(streetNorm, number, scoped)
@@ -169,8 +170,8 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
169
170
  lon: row.lon,
170
171
  source: row.source,
171
172
  release: row.release,
172
- ...(row.locality_norm ? { localityNorm: row.locality_norm } : {}),
173
- ...(row.postcode ? { postcode: row.postcode } : {}),
173
+ localityNorm: row.locality_norm || null,
174
+ postcode: row.postcode || null,
174
175
  }
175
176
  }
176
177
 
@@ -187,7 +188,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
187
188
  postcode?: string
188
189
  locality?: string
189
190
  localityTail?: NameKey
190
- bbox?: { minLat: number; maxLat: number; minLon: number; maxLon: number }
191
+ bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number } | null
191
192
  }
192
193
  ) {
193
194
  let row = this.#probe(streetNorm, number, query)
@@ -241,22 +242,22 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
241
242
  postcode?: string
242
243
  locality?: string
243
244
  localityTail?: NameKey
244
- bbox?: { minLat: number; maxLat: number; minLon: number; maxLon: number }
245
+ bbox: { minLat: number; maxLat: number; minLon: number; maxLon: number } | null
245
246
  }
246
- ): AddressPointRow | undefined {
247
- let row: AddressPointRow | undefined
247
+ ): AddressPointRow | null {
248
+ let row: AddressPointRow | null = null
248
249
 
249
250
  if (query.postcode) {
250
251
  const postcode = query.postcode.trim()
251
- const localityKey = query.locality ? this.#localityKey(query.locality) : undefined
252
+ const localityKey = query.locality ? this.#localityKey(query.locality) : null
252
253
 
253
254
  // One postcode can cover several places with the same street and number,
254
255
  // so a row whose locality agrees with the query is tried first.
255
256
  // The postcode-only row answers only if `#scopeContradicts` accepts it.
256
- const agreeing = localityKey ? this.#byPostcodeLocality!(postcode, localityKey, streetNorm, number) : undefined
257
+ const agreeing = localityKey ? this.#byPostcodeLocality!(postcode, localityKey, streetNorm, number) : null
257
258
  const candidate = agreeing ?? this.#byPostcode!(postcode, streetNorm, number)
258
259
 
259
- row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : undefined
260
+ row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : null
260
261
  }
261
262
 
262
263
  if (!row && query.locality) {
@@ -274,7 +275,7 @@ export class AddressPointSqliteLookup<DB extends AddressPointDatabase = AddressP
274
275
  const candidate = this.#byBbox!(streetNorm, number, b.minLat, b.maxLat, b.minLon, b.maxLon)
275
276
 
276
277
  // A row with its own scope that the scoped probes missed is a different address in the same box.
277
- row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : undefined
278
+ row = candidate && !this.#scopeContradicts(candidate, query) ? candidate : null
278
279
  }
279
280
 
280
281
  return row
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { COUNTRY_POPULATION } from "@mailwoman/codex/country"
8
- import { pathExists } from "@mailwoman/core/fs/readers"
8
+ import { pathExists } from "@mailwoman/core/fs/readers/stat"
9
9
  import { removePath } from "@mailwoman/core/fs/writers"
10
10
  import { stringifyJSON } from "@mailwoman/core/json"
11
11
  import { DatabaseClient } from "@mailwoman/sqlite/client"
@@ -32,19 +32,6 @@ import { resurrectCurrencyHoles } from "#currency-backfill"
32
32
  import type { WOFDatabase } from "#schema"
33
33
  import { normalizeLocalityForKey } from "#street/normalize"
34
34
 
35
- /**
36
- * Re-exports the country display-name staging pass from the candidate build's public module.
37
- */
38
- export { stageCountryDisplayNames } from "#candidate/country-display-names"
39
- /**
40
- * Re-exports the gloss detector's excluded placetypes and default key-count threshold.
41
- */
42
- export { GLOSS_EXCLUDED_PLACETYPES, GLOSS_KEY_THRESHOLD } from "#candidate/name-roles"
43
- /**
44
- * Re-exports the per-place record that every candidate staging pass writes its rows from.
45
- */
46
- export type { PlaceAttrs } from "#candidate/place-attrs"
47
-
48
35
  /**
49
36
  * Options for {@link buildCandidateTable}.
50
37
  */
@@ -111,7 +98,8 @@ export interface BuildCandidateOptions {
111
98
 
112
99
  /**
113
100
  * The deprecated placetypes eligible for restoration.
114
- * The default is `locality`.
101
+ *
102
+ * @defaultValue `locality`
115
103
  */
116
104
  deadPlacetypes?: readonly string[]
117
105
  }
@@ -123,9 +111,10 @@ export interface BuildCandidateOptions {
123
111
 
124
112
  /**
125
113
  * The key-count threshold for the gloss anomaly detector.
126
- * The default is {@link GLOSS_KEY_THRESHOLD}.
127
114
  *
128
115
  * Tests override it with a value sized for fixtures.
116
+ *
117
+ * @defaultValue {@linkcode GLOSS_KEY_THRESHOLD}
129
118
  */
130
119
  glossKeyThreshold?: number
131
120
  }
@@ -178,9 +167,9 @@ export interface BuildCandidateResult {
178
167
  /**
179
168
  * The count of places that received an `importance` score.
180
169
  *
181
- * It is `undefined` when no score source was given.
170
+ * It is `null` when no score source was given.
182
171
  */
183
- importanceScored?: number
172
+ importanceScored: number | null
184
173
 
185
174
  /**
186
175
  * The count of places that matched a scored group by name key, country and placetype
@@ -188,7 +177,7 @@ export interface BuildCandidateResult {
188
177
  *
189
178
  * A large change between rebuilds means the score source and the admin source have diverged.
190
179
  */
191
- importanceFiltered?: number
180
+ importanceFiltered: number | null
192
181
 
193
182
  /**
194
183
  * The count of alias rows the gloss anomaly detector stamped `name_role = 'gloss'`.
@@ -257,7 +246,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
257
246
  return id
258
247
  }
259
248
 
260
- let importance: ReturnType<typeof loadImportanceIndex> | undefined
249
+ let importance: ReturnType<typeof loadImportanceIndex> | null = null
261
250
 
262
251
  if (opts.importance) {
263
252
  progress("importance", `loading place_importance from ${opts.importance}`)
@@ -603,6 +592,7 @@ export async function buildCandidateTable(opts: BuildCandidateOptions): Promise<
603
592
  ancestorPlaces: sidecar.ancestorPlaces + nLocalityScoped,
604
593
  intervalPlaces: sidecar.intervalPlaces,
605
594
  ...roles,
606
- ...(importance ? { importanceScored: importance.matched, importanceFiltered: importance.refused } : {}),
595
+ importanceScored: importance ? importance.matched : null,
596
+ importanceFiltered: importance ? importance.refused : null,
607
597
  }
608
598
  }
package/lib/build/slim.ts CHANGED
@@ -4,7 +4,7 @@
4
4
  * @author Teffen Ellis, et al.
5
5
  */
6
6
 
7
- import { statPath, pathExists } from "@mailwoman/core/fs/readers"
7
+ import { statPath, pathExists } from "@mailwoman/core/fs/readers/stat"
8
8
  import { temporaryDirectory } from "@mailwoman/core/fs/temporary"
9
9
  import { copyFileTo, removePath } from "@mailwoman/core/fs/writers"
10
10
  import { stringifyJSON } from "@mailwoman/core/json"
@@ -140,7 +140,7 @@ export class ImportanceIndex {
140
140
 
141
141
  if (!group) return null
142
142
 
143
- let best: ScoredPlace | undefined
143
+ let best: ScoredPlace | null = null
144
144
  let bestKm = Infinity
145
145
 
146
146
  for (const place of group) {
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { expandPlacetypeFilter } from "@mailwoman/codex/placetype-map"
8
+ import type { VariantAliasExemption } from "@mailwoman/core/geocode"
8
9
  import { type Ancestor, type GazetteerArtifactCoverage, referentialFromPopulation } from "@mailwoman/core/resolver"
9
10
  import { allRows } from "@mailwoman/core/utils"
10
11
  import { jaroWinkler, levenshteinSimilarity } from "@mailwoman/match/comparators"
@@ -31,9 +32,6 @@ import { hasColumn, hasTable } from "#sqlite-utils"
31
32
  import { type NameKey, normalizeLocalityForKey, stripLocalityQualifier } from "#street/normalize"
32
33
  import type { FindPlaceQuery, PlaceCandidate, PlaceLookup, WOFPlacetype } from "#types"
33
34
 
34
- export { rankByPrimaryPreference } from "#primary-preference"
35
- export type { RankedRow } from "#primary-preference"
36
-
37
35
  /**
38
36
  * Where a {@link WOFCandidateTableLookup} reads from
39
37
  * (a read-only `candidate.db` or a caller-held connection) and how it ranks.
@@ -41,9 +39,13 @@ export type { RankedRow } from "#primary-preference"
41
39
  export interface WOFCandidateTableLookupOpts extends SQLiteLookupOptions<CandidateDatabase> {
42
40
  /**
43
41
  * Exempt `name_role = 'variant'` aliases from the cross-country primary-preference penalty.
44
- * No-ops without the role column and is off by default.
42
+ *
43
+ * - No-ops without the role column.
44
+ * - `"not_applied"` penalizes variant aliases like any other.
45
+ *
46
+ * @defaultValue `"applied"`.
45
47
  */
46
- variantAliasExemption?: boolean
48
+ variantAliasExemption?: VariantAliasExemption
47
49
  }
48
50
 
49
51
  type CandidateRow = Pick<
@@ -106,7 +108,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
106
108
  */
107
109
  readonly #nameKeyExistsProbe: ReturnType<DatabaseClient["prepare"]> | undefined
108
110
 
109
- readonly artifactCoverage: GazetteerArtifactCoverage | undefined
111
+ readonly artifactCoverage: GazetteerArtifactCoverage | null
110
112
 
111
113
  readonly #importanceSelect: string
112
114
 
@@ -158,7 +160,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
158
160
  // Optional columns are detected once here for hot-path reads.
159
161
  this.#importanceSelect = hasColumn(this.database, "candidate", "importance") ? ", importance" : ""
160
162
  this.#hasNameRole = hasColumn(this.database, "candidate", "name_role")
161
- this.#variantAliasExemption = opts.variantAliasExemption === true
163
+ this.#variantAliasExemption = (opts.variantAliasExemption ?? "applied") === "applied"
162
164
  this.#roleSelect = this.#hasNameRole ? ", name_role" : ""
163
165
 
164
166
  if (hasTable(this.database, CANDIDATE_ANCESTOR_TABLE)) {
@@ -227,7 +229,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
227
229
  return label
228
230
  }
229
231
 
230
- #qualifierRegionIDs(qualifier: string, country: string | undefined): Set<number> {
232
+ #qualifierRegionIDs(qualifier: string, country: string | null): Set<number> {
231
233
  const ids = new Set<number>()
232
234
 
233
235
  if (!this.#qualifierProbe) return ids
@@ -264,7 +266,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
264
266
  #applyAdminContainment(
265
267
  rows: Array<RankedRow<CandidateRow>>,
266
268
  qualifier: string,
267
- country: string | undefined,
269
+ country: string | null,
268
270
  opts: {
269
271
  nameKey: NameKey
270
272
  strippedKey: NameKey
@@ -425,8 +427,20 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
425
427
  country: query.country?.toUpperCase() ?? "",
426
428
  lat: Number(hit.latitude),
427
429
  lon: Number(hit.longitude),
430
+ parent_id: null,
428
431
  score: 1,
432
+ distanceKm: null,
429
433
  exactMatch: true,
434
+ prominence: null,
435
+ population: null,
436
+ referential: null,
437
+ encyclopedic: null,
438
+ importance: null,
439
+ bbox: null,
440
+ mismatch: null,
441
+ containedByQualifier: null,
442
+ regionScopeMiss: null,
443
+ variantAliasExempted: null,
430
444
  },
431
445
  ]
432
446
  }
@@ -482,13 +496,13 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
482
496
  filterParams.push(...shapeParams)
483
497
 
484
498
  // Region scope is optional (`region_id = parentID`); the fallback stays unscoped.
485
- const regionParentID = query.parentID || undefined
499
+ const regionParentID = query.parentID || null
486
500
 
487
- const probe = (nk: string, regionID: number | undefined, countryID?: number): Array<RankedRow<CandidateRow>> => {
501
+ const probe = (nk: string, regionID: number | null, countryID?: number): Array<RankedRow<CandidateRow>> => {
488
502
  const conds = ["name_key = ?", ...filters]
489
503
  const params: Array<string | number> = [nk, ...filterParams]
490
504
 
491
- if (regionID !== undefined) {
505
+ if (regionID !== null) {
492
506
  conds.push("region_id = ?")
493
507
  params.push(regionID)
494
508
  }
@@ -508,7 +522,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
508
522
  return rankByPrimaryPreference(fetched, limit, undefined, this.#idToPlacetype, this.#variantAliasExemption)
509
523
  }
510
524
 
511
- const cascade = (regionID: number | undefined): Array<RankedRow<CandidateRow>> => {
525
+ const cascade = (regionID: number | null): Array<RankedRow<CandidateRow>> => {
512
526
  let rows = probe(nameKey, regionID)
513
527
 
514
528
  if (!rows.length) {
@@ -567,8 +581,8 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
567
581
 
568
582
  let regionScopeMiss = false
569
583
 
570
- if (!rows.length && regionParentID !== undefined) {
571
- rows = cascade(undefined)
584
+ if (!rows.length && regionParentID !== null) {
585
+ rows = cascade(null)
572
586
  regionScopeMiss = rows.length > 0
573
587
  }
574
588
 
@@ -604,7 +618,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
604
618
 
605
619
  // Admin-containment re-rank runs last.
606
620
  if (query.regionQualifier?.trim() && this.#qualifierProbe && this.#wantsLocality(query.placetype)) {
607
- rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country, {
621
+ rows = this.#applyAdminContainment(rows, query.regionQualifier.trim(), query.country ?? null, {
608
622
  nameKey,
609
623
  strippedKey: normalizeLocalityForKey(stripLocalityQualifier(text)),
610
624
  shapeFilters,
@@ -616,6 +630,7 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
616
630
  const candidates = rows.map((row): PlaceCandidate => {
617
631
  const hasBbox = row.min_lat != null && row.max_lat != null && row.min_lon != null && row.max_lon != null
618
632
  const parent = this.#ancestorLineage(Number(row.spr_id))[0]
633
+ const hasPopulation = row.population !== null && row.population > 0
619
634
 
620
635
  return {
621
636
  id: Number(row.spr_id),
@@ -626,37 +641,35 @@ export class WOFCandidateTableLookup extends SQLiteLookup<CandidateDatabase> imp
626
641
  lat: Number(row.latitude),
627
642
  lon: Number(row.longitude),
628
643
 
629
- ...(parent ? { parent_id: Number(parent.id) } : {}),
644
+ parent_id: parent ? Number(parent.id) : null,
630
645
 
631
646
  score: -Number(row.neg_rank),
647
+ distanceKm: null,
632
648
 
633
649
  prominence: -Number(row.effectiveNegRank),
634
650
 
635
651
  exactMatch: !row.demoted && !row.fuzzy,
636
652
 
637
- ...(regionScopeMiss ? { regionScopeMiss: true } : {}),
653
+ regionScopeMiss: regionScopeMiss ? true : null,
638
654
 
639
- ...(row.containedByQualifier === undefined ? {} : { containedByQualifier: row.containedByQualifier }),
655
+ containedByQualifier: row.containedByQualifier ?? null,
640
656
 
641
- ...(row.variantExempted ? { variantAliasExempted: true as const } : {}),
657
+ variantAliasExempted: row.variantExempted ? true : null,
642
658
 
643
- ...(row.population === null || row.population <= 0
644
- ? {}
645
- : { population: row.population, referential: referentialFromPopulation(row.population) }),
659
+ population: hasPopulation ? row.population : null,
660
+ referential: hasPopulation ? referentialFromPopulation(row.population) : null,
661
+ encyclopedic: null,
646
662
 
647
- ...(typeof row.importance === "number" && Number.isFinite(row.importance)
648
- ? { importance: row.importance }
649
- : {}),
650
- ...(hasBbox
663
+ importance: typeof row.importance === "number" && Number.isFinite(row.importance) ? row.importance : null,
664
+ bbox: hasBbox
651
665
  ? {
652
- bbox: {
653
- minLat: Number(row.min_lat),
654
- maxLat: Number(row.max_lat),
655
- minLon: Number(row.min_lon),
656
- maxLon: Number(row.max_lon),
657
- },
666
+ minLat: Number(row.min_lat),
667
+ maxLat: Number(row.max_lat),
668
+ minLon: Number(row.min_lon),
669
+ maxLon: Number(row.max_lon),
658
670
  }
659
- : {}),
671
+ : null,
672
+ mismatch: null,
660
673
  }
661
674
  })
662
675
 
@@ -135,7 +135,7 @@ export function ownNameSimilarity(primaryKey: string, aliasKey: string): number
135
135
  const primary = romanizeNameKey(primaryKey)
136
136
  const alias = romanizeNameKey(aliasKey)
137
137
 
138
- if (primary == null || alias == null || !primary || !alias) return null
138
+ if (!primary || !alias || !primary || !alias) return null
139
139
 
140
140
  return levenshteinSimilarity(expandNameAbbreviations(primary), expandNameAbbreviations(alias))
141
141
  }
@@ -56,7 +56,7 @@ export function candidateFromSearchRow(
56
56
 
57
57
  // Proximity boost applies only when the query includes `near` and the candidate has real coordinates.
58
58
  // The decay is tunable via proximityBoost + proximityScaleKm.
59
- let distanceKm: number | undefined
59
+ let distanceKm: number | null = null
60
60
  // The best decayed-distance term over `near` and every `bias` point wins, each scaled by its weight.
61
61
  let proximityTerm = 0
62
62
 
@@ -96,6 +96,14 @@ export function candidateFromSearchRow(
96
96
  // additive units, so hints can win a cross-country postcode tie without a hard filter.
97
97
  const prominence = popTerm + proximityTerm
98
98
 
99
+ const population = row.population !== null && row.population > 0 ? row.population : null
100
+
101
+ // The referential key derives from this row's population and no other field,
102
+ // so it cannot drift from the ordering.
103
+ // Consumers read the encyclopedic value.
104
+ // No ranking site reads it.
105
+ // The bbox gives parity with the wasm lookup.
106
+ // Without it the region→bbox constraint is dead and disambiguation falls to population ranking.
99
107
  const candidate: PlaceCandidate = {
100
108
  id: row.id,
101
109
  prominence,
@@ -104,35 +112,27 @@ export function candidateFromSearchRow(
104
112
  country: row.country ?? "",
105
113
  lat: row.lat ?? 0,
106
114
  lon: row.lon ?? 0,
107
- parent_id: row.parent_id ?? undefined,
115
+ parent_id: row.parent_id ?? null,
108
116
  score,
109
- }
110
-
111
- if (distanceKm !== undefined) {
112
- candidate.distanceKm = distanceKm
113
- }
114
-
115
- if (row.population !== null && row.population > 0) {
116
- candidate.population = row.population
117
- // The referential ranking key is derived as a pure function of this row's population,
118
- // so it cannot drift from the ordering.
119
- candidate.referential = referentialFromPopulation(row.population)
120
- }
121
-
122
- // Returned for consumers (annotations / API surfaces); no ranking site reads it.
123
- if (row.encyclopedic !== null) {
124
- candidate.encyclopedic = row.encyclopedic
125
- }
126
-
127
- // Candidate bbox for parity with the wasm lookup. Without it the Node backend's
128
- // region→bbox constraint is dead and disambiguation falls to population ranking.
129
- if (row.min_latitude != null && row.max_latitude != null && row.min_longitude != null && row.max_longitude != null) {
130
- candidate.bbox = {
131
- minLat: row.min_latitude,
132
- maxLat: row.max_latitude,
133
- minLon: row.min_longitude,
134
- maxLon: row.max_longitude,
135
- }
117
+ distanceKm,
118
+ exactMatch: null,
119
+ population,
120
+ referential: population === null ? null : referentialFromPopulation(population),
121
+ encyclopedic: row.encyclopedic ?? null,
122
+ importance: null,
123
+ bbox:
124
+ row.min_latitude != null && row.max_latitude != null && row.min_longitude != null && row.max_longitude != null
125
+ ? {
126
+ minLat: row.min_latitude,
127
+ maxLat: row.max_latitude,
128
+ minLon: row.min_longitude,
129
+ maxLon: row.max_longitude,
130
+ }
131
+ : null,
132
+ mismatch: null,
133
+ containedByQualifier: null,
134
+ regionScopeMiss: null,
135
+ variantAliasExempted: null,
136
136
  }
137
137
 
138
138
  return candidate
package/lib/capitals.ts CHANGED
@@ -108,9 +108,9 @@ export class CapitalIndex {
108
108
  * an unknown country, or no matching entry.
109
109
  */
110
110
  levelOfPlace(
111
- name: string | null | undefined,
112
- country: string | null | undefined,
113
- latitude: number | null | undefined,
111
+ name: string | null,
112
+ country: string | null,
113
+ latitude: number | null,
114
114
  longitude: number | null | undefined
115
115
  ): CapitalLevel {
116
116
  if (!name || !country || typeof latitude !== "number" || typeof longitude !== "number") return CAPITAL_LEVEL.none
@@ -66,25 +66,25 @@ export interface BuildCoincidentRolesOpts {
66
66
  /**
67
67
  * Drop + rebuild the table if it already exists.
68
68
  *
69
- * Default true (the build is cheap + idempotent).
69
+ * @defaultValue `true` (the build is cheap + idempotent)
70
70
  */
71
71
  drop?: boolean
72
72
  /**
73
73
  * Relative tolerance: a pair is kept when centroid distance ≤ `toleranceFraction × bbox-diagonal`.
74
74
  *
75
- * Default 0.15.
75
+ * @defaultValue `0.15`
76
76
  */
77
77
  toleranceFraction?: number
78
78
  /**
79
79
  * Floor (km) under the relative tolerance, so small-bbox city-states still qualify.
80
80
  *
81
- * Default 12.
81
+ * @defaultValue `12`
82
82
  */
83
83
  minToleranceKm?: number
84
84
  /**
85
85
  * Centroid distance (km) below which a region-tier pair is classed `city-state` (metadata only).
86
86
  *
87
- * Default 2.
87
+ * @defaultValue `2`
88
88
  */
89
89
  cityStateMaxKm?: number
90
90
  onProgress?: (phase: string, detail?: string) => void
@@ -320,6 +320,17 @@ export function loadCoincidentLocalities<DB>(db: DatabaseClient<DB>): Map<number
320
320
  relationshipType: r.relationshipType,
321
321
  population: r.population,
322
322
  distanceKm: r.distanceKm,
323
+ parent_id: null,
324
+ prominence: null,
325
+ referential: null,
326
+ encyclopedic: null,
327
+ importance: null,
328
+ exactMatch: null,
329
+ mismatch: null,
330
+ resolutionQuality: null,
331
+ containedByQualifier: null,
332
+ regionScopeMiss: null,
333
+ variantAliasExempted: null,
323
334
  }
324
335
 
325
336
  const list = map.get(r.adminID)
@@ -9,6 +9,7 @@
9
9
  * other.
10
10
  */
11
11
 
12
+ import type { KeyValueMetaTable } from "@mailwoman/sqlite/database-schema"
12
13
  import type { Kysely } from "kysely"
13
14
 
14
15
  import { ADDRESS_CONVENTION_TABLE } from "#convention"
@@ -30,17 +31,9 @@ export interface AddressConventionTable {
30
31
  source: string
31
32
  }
32
33
 
33
- /**
34
- * Key/value provenance for the sealed artifact.
35
- */
36
- export interface ConventionMetaTable {
37
- key: string
38
- value: string | null
39
- }
40
-
41
34
  export interface ConventionDatabase {
42
35
  address_convention: AddressConventionTable
43
- meta: ConventionMetaTable
36
+ meta: KeyValueMetaTable
44
37
  }
45
38
 
46
39
  /**
package/lib/convention.ts CHANGED
@@ -79,10 +79,10 @@ export type Strategy = (query: FindPlaceQuery, convention: ResolvedConvention) =
79
79
 
80
80
  /**
81
81
  * Looks up a convention by WOF polygon id.
82
- * `get` returns `undefined` when the polygon has no override.
82
+ * `get` returns `null` when the polygon has no override.
83
83
  */
84
84
  export interface ConventionSource {
85
- get(wofID: number): Convention | undefined
85
+ get(wofID: number): Convention | null
86
86
  }
87
87
 
88
88
  /**
@@ -95,8 +95,8 @@ export class SeedConventionSource implements ConventionSource {
95
95
  this.#rows = new Map(Object.entries(rows).map(([k, v]) => [Number(k), v]))
96
96
  }
97
97
 
98
- get(wofID: number): Convention | undefined {
99
- return this.#rows.get(wofID)
98
+ get(wofID: number): Convention | null {
99
+ return this.#rows.get(wofID) ?? null
100
100
  }
101
101
  }
102
102
 
@@ -106,7 +106,7 @@ export class SeedConventionSource implements ConventionSource {
106
106
  * A layer's `candidateStrategies` replaces the whole list.
107
107
  * `scoringWeights` merges key by key, so a layer can change one weight without restating the others.
108
108
  */
109
- export function mergeConventions(base: Convention, ...overrides: Array<Convention | undefined>): Convention {
109
+ export function mergeConventions(base: Convention, ...overrides: Array<Convention | null | undefined>): Convention {
110
110
  const out: Convention = {
111
111
  candidateStrategies: base.candidateStrategies ? [...base.candidateStrategies] : undefined,
112
112
  scoringWeights: base.scoringWeights ? { ...base.scoringWeights } : undefined,
@@ -115,11 +115,11 @@ export function mergeConventions(base: Convention, ...overrides: Array<Conventio
115
115
  for (const o of overrides) {
116
116
  if (!o) continue
117
117
 
118
- if (o.candidateStrategies !== undefined) {
118
+ if (o.candidateStrategies) {
119
119
  out.candidateStrategies = [...o.candidateStrategies]
120
120
  }
121
121
 
122
- if (o.scoringWeights !== undefined) {
122
+ if (o.scoringWeights) {
123
123
  out.scoringWeights = { ...(out.scoringWeights ?? WORLD_DEFAULT.scoringWeights), ...o.scoringWeights }
124
124
  }
125
125
  }