@mailwoman/resolver-wof-sqlite 10.1.0 → 10.2.1

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
@@ -146,8 +146,8 @@ export async function writeGazetteerCoverageManifest(
146
146
  facts.coverage.map((f) => ({
147
147
  country: f.country.toUpperCase(),
148
148
  hard_filter_safe: f.hardFilterSafe ? 1 : 0,
149
- hard_resolve_rate: f.hardResolveRate ?? null,
150
- sample_size: f.sampleSize ?? null,
149
+ hard_resolve_rate: f.hardResolveRate,
150
+ sample_size: f.sampleSize,
151
151
  measured_at: f.measuredAt,
152
152
  source: f.source,
153
153
  }))
@@ -180,11 +180,11 @@ export async function writeGazetteerCoverageManifest(
180
180
  *
181
181
  * The reads are synchronous because the candidate lookup calls this from its constructor.
182
182
  */
183
- export function readGazetteerCoverageManifest<DB>(db: DatabaseClient<DB>): GazetteerArtifactCoverage | undefined {
183
+ export function readGazetteerCoverageManifest<DB>(db: DatabaseClient<DB>): GazetteerArtifactCoverage | null {
184
184
  const hasCoverage = hasTable(db, COUNTRY_COVERAGE_TABLE)
185
185
  const hasBBox = hasTable(db, COUNTRY_BBOX_TABLE)
186
186
 
187
- if (!hasCoverage && !hasBBox) return undefined
187
+ if (!hasCoverage && !hasBBox) return null
188
188
 
189
189
  const countryCoverage = new Map<string, CountryCoverageFact>()
190
190
 
@@ -201,8 +201,8 @@ export function readGazetteerCoverageManifest<DB>(db: DatabaseClient<DB>): Gazet
201
201
  countryCoverage.set(country, {
202
202
  country,
203
203
  hardFilterSafe: Number(row.hard_filter_safe) !== 0,
204
- ...(row.hard_resolve_rate === null ? {} : { hardResolveRate: Number(row.hard_resolve_rate) }),
205
- ...(row.sample_size === null ? {} : { sampleSize: Number(row.sample_size) }),
204
+ hardResolveRate: row.hard_resolve_rate === null ? null : Number(row.hard_resolve_rate),
205
+ sampleSize: row.sample_size === null ? null : Number(row.sample_size),
206
206
  measuredAt: String(row.measured_at),
207
207
  source: String(row.source),
208
208
  })
@@ -6,7 +6,7 @@
6
6
  * Restores deprecated WOF localities only when an independent source attests them.
7
7
  */
8
8
 
9
- import { pathExists } from "@mailwoman/core/fs/readers"
9
+ import { pathExists } from "@mailwoman/core/fs/readers/stat"
10
10
  import { isStrictlyFiner } from "@mailwoman/core/resources/whosonfirst"
11
11
  import { haversineKm } from "@mailwoman/spatial"
12
12
  import type { DatabaseClient } from "@mailwoman/sqlite/client"
@@ -27,7 +27,7 @@ const CURRENCY_BACKFILL_POP_FLOOR = 1000
27
27
  * Restore a deprecated locality only when no nearby live namesake exists and GeoNames
28
28
  * attests the same folded name above {@link CURRENCY_BACKFILL_POP_FLOOR}.
29
29
  */
30
- export interface CurrencyBackfillOutcomes {
30
+ export interface CurrencyBackfillResults {
31
31
  judged: number
32
32
  blocked: number
33
33
  unattested: number
@@ -36,14 +36,14 @@ export interface CurrencyBackfillOutcomes {
36
36
  }
37
37
 
38
38
  /**
39
- * One country's read of the check — every dead name judged, by outcome and by the dead
39
+ * One country's read of the check — every dead name judged, by result and by the dead
40
40
  * row's placetype — so a census can say what a wider query admits.
41
41
  */
42
- export interface CurrencyBackfillCountryReport extends CurrencyBackfillOutcomes {
42
+ export interface CurrencyBackfillCountryReport extends CurrencyBackfillResults {
43
43
  country: string
44
44
  deadPlacetypes: readonly string[]
45
45
  dumpPresent: boolean
46
- byDeadPlacetype: Record<string, CurrencyBackfillOutcomes>
46
+ byDeadPlacetype: Record<string, CurrencyBackfillResults>
47
47
  /**
48
48
  * The first resurrected names, `placetype:name`, capped because the report is a receipt and not the table.
49
49
  */
@@ -57,7 +57,7 @@ const REPORT_SAMPLE_SIZE = 25
57
57
  */
58
58
  export const DEFAULT_DEAD_PLACETYPES: readonly string[] = ["locality"]
59
59
 
60
- function emptyOutcomes(): CurrencyBackfillOutcomes {
60
+ function emptyResults(): CurrencyBackfillResults {
61
61
  return { judged: 0, blocked: 0, unattested: 0, floored: 0, resurrected: 0 }
62
62
  }
63
63
 
@@ -73,7 +73,7 @@ export async function resurrectCurrencyHoles(ctx: {
73
73
  ccID: (code: string | null) => number
74
74
  ptID: (pt: string | null) => number
75
75
  regionOf: Map<number, number>
76
- importance: ReturnType<typeof loadImportanceIndex> | undefined
76
+ importance: ReturnType<typeof loadImportanceIndex> | null
77
77
  stageRow: StageRow
78
78
  progress: (phase: string, message: string) => void
79
79
 
@@ -83,7 +83,7 @@ export async function resurrectCurrencyHoles(ctx: {
83
83
  */
84
84
  dryRun?: boolean
85
85
  /**
86
- * Receives one report per country judged (a country with no dump or no dead rows reports zero outcomes).
86
+ * Receives one report per country judged (a country with no dump or no dead rows reports zero results).
87
87
  */
88
88
  onCountry?: (report: CurrencyBackfillCountryReport) => void
89
89
  }): Promise<number> {
@@ -117,7 +117,7 @@ export async function resurrectCurrencyHoles(ctx: {
117
117
  country: cc,
118
118
  deadPlacetypes,
119
119
  dumpPresent: await pathExists(dumpPath),
120
- ...emptyOutcomes(),
120
+ ...emptyResults(),
121
121
  byDeadPlacetype: {},
122
122
  sample: [],
123
123
  }
@@ -180,11 +180,11 @@ export async function resurrectCurrencyHoles(ctx: {
180
180
 
181
181
  const seen = new Set<string>()
182
182
 
183
- const count = (deadPlacetype: string, outcome: keyof CurrencyBackfillOutcomes): void => {
184
- report[outcome] += 1
185
- const bucket = (report.byDeadPlacetype[deadPlacetype] ??= emptyOutcomes())
183
+ const count = (deadPlacetype: string, result: keyof CurrencyBackfillResults): void => {
184
+ report[result] += 1
185
+ const bucket = (report.byDeadPlacetype[deadPlacetype] ??= emptyResults())
186
186
 
187
- bucket[outcome] += 1
187
+ bucket[result] += 1
188
188
  }
189
189
 
190
190
  if (!dryRun) {
@@ -58,7 +58,7 @@ export function exactMatchIDs<DB>(
58
58
  const needle = foldQueryText(trimmed)
59
59
 
60
60
  for (const r of rows) {
61
- if (r.name !== null && foldQueryText(r.name) === needle) {
61
+ if (r.name && foldQueryText(r.name) === needle) {
62
62
  out.add(r.id)
63
63
  }
64
64
  }
package/lib/extracts.ts CHANGED
@@ -213,7 +213,7 @@ export function pickExtractsForPlacetype(
213
213
  */
214
214
  export function pickExtractForPlacetype(
215
215
  extracts: ResolvedExtract[],
216
- placetype: string | undefined,
216
+ placetype: string | null,
217
217
  opts?: {
218
218
  /**
219
219
  * The query's country constraint, when the caller has one.
@@ -54,7 +54,7 @@ export interface AutocompleteSuggestion {
54
54
  * `undefined` means no article, or a pre-v5 binary.
55
55
  * It is never 0.
56
56
  */
57
- encyclopedic?: number
57
+ encyclopedic: number | null
58
58
  wofID: number
59
59
  parentChain: number[]
60
60
  matchDepth: number
@@ -182,7 +182,7 @@ export function autocomplete(fst: FSTMatcher, query: string, opts: AutocompleteO
182
182
  name: entry.name,
183
183
  placetype: entry.placetype,
184
184
  referential: entry.referential,
185
- ...(entry.encyclopedic === undefined ? {} : { encyclopedic: entry.encyclopedic }),
185
+ encyclopedic: entry.encyclopedic,
186
186
  wofID: s.id,
187
187
  parentChain: s.parentIDs,
188
188
  matchDepth: s.matchDepth,
@@ -234,7 +234,7 @@ export async function buildFSTFromWOF(opts: BuildFSTOpts): Promise<{
234
234
 
235
235
  if (excludeSurfaces?.has(tokens.join(" "))) return true
236
236
 
237
- if (excludeAllTokensOf !== undefined && tokens.every((t) => excludeAllTokensOf.has(t))) return true
237
+ if (excludeAllTokensOf && tokens.every((t) => excludeAllTokensOf.has(t))) return true
238
238
 
239
239
  return false
240
240
  }
@@ -272,7 +272,7 @@ export async function buildFSTFromWOF(opts: BuildFSTOpts): Promise<{
272
272
  const existing = nodes[stateID]!.places
273
273
 
274
274
  if (!existing.some((p) => p.wofID === entry.wofID && p.placetype === entry.placetype)) {
275
- if (surfaceCountryCounts !== undefined) {
275
+ if (surfaceCountryCounts) {
276
276
  const count = surfaceCountryCounts.get(tokens.join(" "))
277
277
  existing.push({ ...entry, crossCountryBranches: Math.min(count ?? 1, 255) })
278
278
  } else {
@@ -296,11 +296,12 @@ export async function buildFSTFromWOF(opts: BuildFSTOpts): Promise<{
296
296
  name: row.name,
297
297
  parentChain,
298
298
  referential: split.referential.get(row.id) ?? 0,
299
- // Spread rather than assigned, so a place with no Wikipedia article has no field instead of a zero.
300
- // The serializer's per-place presence bit reads `!== undefined`.
301
- ...(encyclopedic === undefined ? {} : { encyclopedic }),
299
+ // A place with no Wikipedia article is `null` rather than a zero.
300
+ // The serializer's per-place presence bit reads `!== null`.
301
+ encyclopedic: encyclopedic ?? null,
302
302
  lat: row.latitude,
303
303
  lon: row.longitude,
304
+ crossCountryBranches: null,
304
305
  }
305
306
 
306
307
  const primaryTokens = normalizeTokens(row.name)
@@ -352,9 +353,10 @@ export async function buildFSTFromWOF(opts: BuildFSTOpts): Promise<{
352
353
  sourceDB: dbPath,
353
354
  sourceDBMD5: source.md5,
354
355
  sourceDBBytes: source.bytes,
355
- ...(excludeSurfaces !== undefined || excludeAllTokensOf !== undefined
356
+ modelCardVersion: null,
357
+ ...(excludeSurfaces || excludeAllTokensOf
356
358
  ? { exclusionPolicy: opts.exclusionPolicy ?? "unspecified", excludedInsertions: excludedCount }
357
- : {}),
359
+ : { exclusionPolicy: null, excludedInsertions: null }),
358
360
  }
359
361
 
360
362
  return {
@@ -143,8 +143,8 @@ export function deserializeFSTWeb(input: ArrayBuffer | Uint8Array): FSTMatcher {
143
143
  lat: view.getFloat32(pp + 16, true),
144
144
  lon: view.getFloat32(pp + 20, true),
145
145
  parentChain,
146
- ...(hasAmbiguity ? { crossCountryBranches: view.getUint8(pp + 6) } : {}),
147
- ...(hasEncyclopedic ? { encyclopedic: view.getFloat32(pp + ENCYCLOPEDIC_OFFSET, true) } : {}),
146
+ crossCountryBranches: hasAmbiguity ? view.getUint8(pp + 6) : null,
147
+ encyclopedic: hasEncyclopedic ? view.getFloat32(pp + ENCYCLOPEDIC_OFFSET, true) : null,
148
148
  }
149
149
  }
150
150
 
@@ -154,25 +154,25 @@ export function deserializeFSTWeb(input: ArrayBuffer | Uint8Array): FSTMatcher {
154
154
  return FSTMatcher.fromNodes(nodes)
155
155
  }
156
156
 
157
- export function readFSTProvenanceWeb(input: ArrayBuffer | Uint8Array): FSTProvenance | undefined {
157
+ export function readFSTProvenanceWeb(input: ArrayBuffer | Uint8Array): FSTProvenance | null {
158
158
  const bytes = input instanceof ArrayBuffer ? new Uint8Array(input) : input
159
159
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
160
160
  const decoder = new TextDecoder("utf-8")
161
161
 
162
- if (bytes.byteLength < HEADER_SIZE) return undefined
162
+ if (bytes.byteLength < HEADER_SIZE) return null
163
163
  const version = view.getUint16(4, true)
164
164
 
165
- if (version < VERSION_WITH_METADATA) return undefined
165
+ if (version < VERSION_WITH_METADATA) return null
166
166
  const provenanceOffset = view.getUint32(28, true)
167
167
 
168
- if (provenanceOffset === 0 || provenanceOffset >= bytes.byteLength) return undefined
168
+ if (provenanceOffset === 0 || provenanceOffset >= bytes.byteLength) return null
169
169
 
170
170
  try {
171
171
  const jsonLen = view.getUint32(provenanceOffset, true)
172
172
  const jsonStr = decoder.decode(bytes.subarray(provenanceOffset + 4, provenanceOffset + 4 + jsonLen))
173
173
 
174
- return tryParsingJSON<FSTProvenance>(jsonStr) ?? undefined
174
+ return tryParsingJSON<FSTProvenance>(jsonStr) ?? null
175
175
  } catch {
176
- return undefined
176
+ return null
177
177
  }
178
178
  }
@@ -6,13 +6,14 @@
6
6
  * Does an `fst-*.bin` still match the gazetteer it was built from?
7
7
  */
8
8
 
9
- import { pathExists, readFileRange, readLocalTextFile, statPath } from "@mailwoman/core/fs/readers"
9
+ import { readFileRange, readLocalTextFile } from "@mailwoman/core/fs/readers"
10
+ import { pathExists, statPath } from "@mailwoman/core/fs/readers/stat"
10
11
  import { writeLocalTextFile } from "@mailwoman/core/fs/writers"
11
12
  import { md5File } from "@mailwoman/core/hash"
12
13
  import { tryParsingJSON } from "@mailwoman/core/json"
13
14
  import { basename, resolvePath, type PathBuilderLike } from "path-ts"
14
15
 
15
- import { FST_FORMAT_VERSION } from "#fst/serialize"
16
+ import { FST_FORMAT_VERSION } from "#fst/format"
16
17
  import type { FSTProvenance } from "#fst/types"
17
18
 
18
19
  /**
@@ -48,7 +49,7 @@ export interface FSTSourceIdentity {
48
49
  */
49
50
  export interface FSTStampFields {
50
51
  formatVersion: number
51
- provenance: FSTProvenance | undefined
52
+ provenance: FSTProvenance | null
52
53
  }
53
54
 
54
55
  /**
@@ -59,37 +60,37 @@ export interface FSTExpectation {
59
60
  /**
60
61
  * Serializer version to require, defaulting to {@link FST_FORMAT_VERSION}.
61
62
  */
62
- formatVersion?: number
63
- exclusionPolicy?: string
63
+ formatVersion?: number | null
64
+ exclusionPolicy?: string | null
64
65
  }
65
66
 
66
67
  /**
67
68
  * Read an artifact's stamp without deserializing it.
68
69
  */
69
- export async function peekFSTStampFields(path: PathBuilderLike): Promise<FSTStampFields | undefined> {
70
- if (!(await pathExists(path))) return undefined
70
+ export async function peekFSTStampFields(path: PathBuilderLike): Promise<FSTStampFields | null> {
71
+ if (!(await pathExists(path))) return null
71
72
  const size = (await statPath(path)).size
72
73
 
73
- if (size < HEADER_SIZE) return undefined
74
+ if (size < HEADER_SIZE) return null
74
75
  const header = await readFileRange(path, 0, HEADER_SIZE)
75
76
 
76
- if (header.readUInt32LE(0) !== MAGIC) return undefined
77
+ if (header.readUInt32LE(0) !== MAGIC) return null
77
78
  const formatVersion = header.readUInt16LE(4)
78
79
 
79
- if (formatVersion < MIN_STAMPED_FORMAT_VERSION) return { formatVersion, provenance: undefined }
80
+ if (formatVersion < MIN_STAMPED_FORMAT_VERSION) return { formatVersion, provenance: null }
80
81
  const trailerStart = header.readUInt32LE(PROVENANCE_OFFSET_FIELD)
81
82
 
82
83
  // 0 means this build wrote no trailer and past-EOF means truncated.
83
84
  // Both read as no stamp.
84
- if (trailerStart === 0 || trailerStart + 4 > size) return { formatVersion, provenance: undefined }
85
+ if (trailerStart === 0 || trailerStart + 4 > size) return { formatVersion, provenance: null }
85
86
 
86
87
  const jsonLength = (await readFileRange(path, trailerStart, 4)).readUInt32LE(0)
87
88
 
88
- if (jsonLength === 0 || trailerStart + 4 + jsonLength > size) return { formatVersion, provenance: undefined }
89
+ if (jsonLength === 0 || trailerStart + 4 + jsonLength > size) return { formatVersion, provenance: null }
89
90
 
90
91
  const json = await readFileRange(path, trailerStart + 4, jsonLength)
91
92
 
92
- return { formatVersion, provenance: tryParsingJSON<FSTProvenance>(json.toString("utf8")) ?? undefined }
93
+ return { formatVersion, provenance: tryParsingJSON<FSTProvenance>(json.toString("utf8")) ?? null }
93
94
  }
94
95
 
95
96
  /**
@@ -106,7 +107,7 @@ export async function readWOFSourceIdentity(
106
107
 
107
108
  if (hit) return hit
108
109
  const sidecarPath = `${path}.md5`
109
- let md5: string | undefined
110
+ let md5: string | null = null
110
111
 
111
112
  if (await pathExists(sidecarPath)) {
112
113
  const sidecarStats = await statPath(sidecarPath)
@@ -141,9 +142,9 @@ export async function readWOFSourceIdentity(
141
142
  const sourceIdentityMemo = new Map<string, FSTSourceIdentity>()
142
143
 
143
144
  /**
144
- * Why an FST artifact is stale against `expected`, or `undefined` when it still matches.
145
+ * Why an FST artifact is stale against `expected`, or `null` when it still matches.
145
146
  */
146
- export function fstStaleReason(fields: FSTStampFields | undefined, expected: FSTExpectation): string | undefined {
147
+ export function fstStaleReason(fields: FSTStampFields | null, expected: FSTExpectation): string | null {
147
148
  if (!fields) return "unreadable or not an FST artifact"
148
149
  const requiredFormat = expected.formatVersion ?? FST_FORMAT_VERSION
149
150
 
@@ -167,15 +168,15 @@ export function fstStaleReason(fields: FSTStampFields | undefined, expected: FST
167
168
  return `source db ${provenance.sourceDBMD5.slice(0, 8)} → ${expected.source.md5.slice(0, 8)} (built ${provenance.builtAt})`
168
169
  }
169
170
 
170
- if (provenance.sourceDBBytes !== undefined && provenance.sourceDBBytes !== expected.source.bytes) {
171
+ if (provenance.sourceDBBytes != null && provenance.sourceDBBytes !== expected.source.bytes) {
171
172
  return `source db size ${provenance.sourceDBBytes} → ${expected.source.bytes} at a matching md5 — one of the two is misrecorded`
172
173
  }
173
174
 
174
- if (expected.exclusionPolicy !== undefined && provenance.exclusionPolicy !== expected.exclusionPolicy) {
175
+ if (expected.exclusionPolicy && provenance.exclusionPolicy !== expected.exclusionPolicy) {
175
176
  return `exclusion policy ${provenance.exclusionPolicy ?? "(none)"} → ${expected.exclusionPolicy}`
176
177
  }
177
178
 
178
- return undefined
179
+ return null
179
180
  }
180
181
 
181
182
  /**
@@ -190,19 +191,19 @@ export async function fstFreshnessWarning({
190
191
  }: {
191
192
  fstPath: PathBuilderLike
192
193
  sourceDBPath: PathBuilderLike
193
- formatVersion?: number
194
- exclusionPolicy?: string
194
+ formatVersion?: number | null
195
+ exclusionPolicy?: string | null
195
196
  rebuildCommand: string
196
- }): Promise<string | undefined> {
197
- if (!(await pathExists(fstPath)) || !(await pathExists(sourceDBPath))) return undefined
197
+ }): Promise<string | null> {
198
+ if (!(await pathExists(fstPath)) || !(await pathExists(sourceDBPath))) return null
198
199
 
199
200
  const reason = fstStaleReason(await peekFSTStampFields(fstPath), {
200
201
  source: await readWOFSourceIdentity(sourceDBPath),
201
- ...(formatVersion === undefined ? {} : { formatVersion }),
202
- ...(exclusionPolicy === undefined ? {} : { exclusionPolicy }),
202
+ formatVersion: formatVersion ?? null,
203
+ exclusionPolicy: exclusionPolicy ?? null,
203
204
  })
204
205
 
205
- return reason === undefined ? undefined : formatFSTStaleWarning({ fstPath, reason, rebuildCommand })
206
+ return reason ? formatFSTStaleWarning({ fstPath, reason, rebuildCommand }) : null
206
207
  }
207
208
 
208
209
  /**
@@ -8,7 +8,9 @@
8
8
  * arrays at accepting states.
9
9
  */
10
10
 
11
- import type { FSTContinuation, FSTMatchResult, FSTQueryResult, PlaceEntry } from "#fst/types"
11
+ import type { AncestrieMatch } from "@mailwoman/ancestrie"
12
+
13
+ import type { FSTContinuation, FSTQueryResult, PlaceEntry } from "#fst/types"
12
14
 
13
15
  interface FSTNode {
14
16
  edges: Map<string, number>
@@ -36,7 +38,7 @@ export class FSTMatcher {
36
38
  return count
37
39
  }
38
40
 
39
- walk(tokens: string[]): FSTMatchResult | null {
41
+ walk(tokens: string[]): AncestrieMatch | null {
40
42
  let stateID = 0
41
43
 
42
44
  for (const token of tokens) {
@@ -54,7 +56,7 @@ export class FSTMatcher {
54
56
  return { stateID, accepted: node.places.length > 0, depth: tokens.length }
55
57
  }
56
58
 
57
- walkFrom(prev: FSTMatchResult, token: string): FSTMatchResult | null {
59
+ walkFrom(prev: AncestrieMatch, token: string): AncestrieMatch | null {
58
60
  const node = this.nodes[prev.stateID]
59
61
 
60
62
  if (!node) return null
@@ -44,11 +44,6 @@ import type { FSTNode } from "#fst/matcher"
44
44
  import { FSTMatcher } from "#fst/matcher"
45
45
  import type { FSTProvenance, PlaceEntry } from "#fst/types"
46
46
 
47
- /**
48
- * Re-exports the format version that {@link serializeFST} writes.
49
- */
50
- export { FST_FORMAT_VERSION } from "#fst/format"
51
-
52
47
  const MAGIC = Buffer.from(FST_MAGIC_BYTES)
53
48
 
54
49
  /**
@@ -119,7 +114,7 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
119
114
 
120
115
  // Flag bit 0 marks that place rows include `crossCountryBranches`.
121
116
  // Readers of a file without it report `undefined`.
122
- const hasAmbiguity = nodes.some((n) => n.places.some((p) => p.crossCountryBranches !== undefined))
117
+ const hasAmbiguity = nodes.some((n) => n.places.some((p) => p.crossCountryBranches != null))
123
118
  MAGIC.copy(buf, pos)
124
119
  pos += 4
125
120
  buf.writeUInt16LE(FST_FORMAT_VERSION, pos)
@@ -191,7 +186,7 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
191
186
  buf.writeUInt8(hasAmbiguity ? Math.min(place.crossCountryBranches ?? 0, 255) : 0, pp + 6)
192
187
  // An absent encyclopedic score writes flag 0 and a 0.0 float.
193
188
  // Readers skip the float when the flag is 0.
194
- const hasEncyclopedic = place.encyclopedic !== undefined
189
+ const hasEncyclopedic = place.encyclopedic != null
195
190
  buf.writeUInt8(hasEncyclopedic ? PLACE_FLAG_HAS_ENCYCLOPEDIC : 0, pp + 7)
196
191
  buf.writeUInt32LE(intern(place.name), pp + 8)
197
192
  buf.writeFloatLE(place.referential, pp + 12)
@@ -202,7 +197,7 @@ export function serializeFST(matcher: FSTMatcher, provenance?: FSTProvenance): B
202
197
  buf.writeUInt32LE(ci < chainLen ? validChain[ci]! : 0, pp + 24 + ci * 4)
203
198
  }
204
199
 
205
- buf.writeFloatLE(hasEncyclopedic ? place.encyclopedic! : 0, pp + ENCYCLOPEDIC_OFFSET)
200
+ buf.writeFloatLE(place.encyclopedic ?? 0, pp + ENCYCLOPEDIC_OFFSET)
206
201
 
207
202
  placeIdx++
208
203
  }
@@ -320,8 +315,8 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
320
315
  lat: buf.readFloatLE(pp + 16),
321
316
  lon: buf.readFloatLE(pp + 20),
322
317
  parentChain,
323
- ...(hasAmbiguity ? { crossCountryBranches: buf.readUInt8(pp + 6) } : {}),
324
- ...(hasEncyclopedic ? { encyclopedic: buf.readFloatLE(pp + ENCYCLOPEDIC_OFFSET) } : {}),
318
+ crossCountryBranches: hasAmbiguity ? buf.readUInt8(pp + 6) : null,
319
+ encyclopedic: hasEncyclopedic ? buf.readFloatLE(pp + ENCYCLOPEDIC_OFFSET) : null,
325
320
  }
326
321
  }
327
322
 
@@ -335,23 +330,23 @@ export function deserializeFST(buf: Buffer): FSTMatcher {
335
330
  * Reads the provenance trailer from an FST binary, or returns `undefined`
336
331
  * when the file has none or it cannot be parsed.
337
332
  */
338
- export function readFSTProvenance(buf: Buffer): FSTProvenance | undefined {
339
- if (buf.length < HEADER_SIZE) return undefined
333
+ export function readFSTProvenance(buf: Buffer): FSTProvenance | null {
334
+ if (buf.length < HEADER_SIZE) return null
340
335
 
341
- if (!buf.subarray(0, 4).equals(MAGIC)) return undefined
336
+ if (!buf.subarray(0, 4).equals(MAGIC)) return null
342
337
  const version = buf.readUInt16LE(4)
343
338
 
344
- if (version < VERSION_WITH_METADATA) return undefined
339
+ if (version < VERSION_WITH_METADATA) return null
345
340
  const provenanceOffset = buf.readUInt32LE(28)
346
341
 
347
- if (provenanceOffset === 0 || provenanceOffset >= buf.length) return undefined
342
+ if (provenanceOffset === 0 || provenanceOffset >= buf.length) return null
348
343
 
349
344
  try {
350
345
  const jsonLen = buf.readUInt32LE(provenanceOffset)
351
346
  const jsonStr = buf.toString("utf8", provenanceOffset + 4, provenanceOffset + 4 + jsonLen)
352
347
 
353
- return tryParsingJSON<FSTProvenance>(jsonStr) ?? undefined
348
+ return tryParsingJSON<FSTProvenance>(jsonStr) ?? null
354
349
  } catch {
355
- return undefined
350
+ return null
356
351
  }
357
352
  }
package/lib/fst/types.ts CHANGED
@@ -21,16 +21,16 @@ export interface PlaceEntry {
21
21
  referential: number
22
22
  /**
23
23
  * The encyclopedic importance in [0, 1], for display only.
24
- * It is `undefined` when unavailable.
24
+ * It is `null` when unavailable.
25
25
  */
26
- encyclopedic?: number
26
+ encyclopedic: number | null
27
27
  lat: number
28
28
  lon: number
29
29
  /**
30
30
  * The number of countries with a place of this surface, clamped to 255.
31
- * It is `undefined` when unavailable.
31
+ * It is `null` when unavailable.
32
32
  */
33
- crossCountryBranches?: number
33
+ crossCountryBranches: number | null
34
34
  }
35
35
 
36
36
  /**
@@ -49,15 +49,6 @@ export type PlacetypeID =
49
49
  | "dependency"
50
50
  | "street_affix"
51
51
 
52
- /**
53
- * The result of walking a token sequence through the FST.
54
- */
55
- export interface FSTMatchResult {
56
- stateID: number
57
- accepted: boolean
58
- depth: number
59
- }
60
-
61
52
  /**
62
53
  * One outgoing edge from an FST state, with the number of places its target state accepts.
63
54
  */
@@ -96,16 +87,16 @@ export interface FSTProvenance {
96
87
  /**
97
88
  * The count of places with an encyclopedic score at build time.
98
89
  *
99
- * It is `undefined` for a build that predates the separate encyclopedic score.
90
+ * It is `null` for a build that predates the separate encyclopedic score.
100
91
  * The value `0` means a current build used a database without encyclopedic scores.
101
92
  */
102
- encyclopedicMatches?: number
93
+ encyclopedicMatches: number | null
103
94
  /**
104
95
  * The {@link ImportanceSplitSource} that records whether the encyclopedic score is real,
105
96
  * reconstructed from a legacy column, or absent.
106
97
  */
107
- importanceSource?: string
108
- sourceDB?: string
98
+ importanceSource: string | null
99
+ sourceDB: string | null
109
100
  /**
110
101
  * The MD5 of the source database's bytes at build time.
111
102
  * The freshness check compares it.
@@ -113,26 +104,26 @@ export interface FSTProvenance {
113
104
  * `sourceDB` by itself cannot detect staleness.
114
105
  * A rebuild replaces the admin database at the same path.
115
106
  *
116
- * The field is `undefined` for artifacts built before the stamp existed.
107
+ * The field is `null` for artifacts built before the stamp existed.
117
108
  * An unknown digest has a different value.
118
109
  */
119
- sourceDBMD5?: string
110
+ sourceDBMD5: string | null
120
111
  /**
121
112
  * The byte size of the source database at build time.
122
113
  *
123
114
  * It detects a truncated source and makes a staleness warning readable.
124
115
  */
125
- sourceDBBytes?: number
126
- modelCardVersion?: string
116
+ sourceDBBytes: number | null
117
+ modelCardVersion: string | null
127
118
  /**
128
119
  * The surface-exclusion policy applied at build time.
129
120
  * It is absent for an uncurated build.
130
121
  */
131
- exclusionPolicy?: string
122
+ exclusionPolicy: string | null
132
123
  /**
133
124
  * The count of name insertions the exclusion policy rejected.
134
125
  */
135
- excludedInsertions?: number
126
+ excludedInsertions: number | null
136
127
  }
137
128
 
138
129
  /**
package/lib/fst.ts CHANGED
@@ -11,4 +11,5 @@ export * from "#fst/format"
11
11
  export * from "#fst/freshness"
12
12
  export * from "#fst/matcher"
13
13
  export * from "#fst/serialize"
14
+ export { FST_FORMAT_VERSION } from "#fst/format"
14
15
  export * from "#fst/types"
package/lib/fts.ts CHANGED
@@ -51,7 +51,7 @@ export function foldQueryText(input: string): string {
51
51
  * Only legacy bags use it.
52
52
  */
53
53
  export function aliasBagExactMatch(altNames: string | null, normalizedQuery: string, anyStrictExact: boolean): boolean {
54
- if (altNames === null || altNames === "" || !normalizedQuery) return false
54
+ if (!altNames || altNames === "" || !normalizedQuery) return false
55
55
 
56
56
  if (altNames.includes(ALIAS_SEPARATOR)) {
57
57
  return altNames.split(ALIAS_SEPARATOR).some((alias) => foldQueryText(alias) === normalizedQuery)
@@ -6,7 +6,7 @@
6
6
 
7
7
  import { isOfficialLanguage } from "@mailwoman/codex/country"
8
8
  import { readUnquotedTSV } from "@mailwoman/core/fs/delimited"
9
- import { pathExists } from "@mailwoman/core/fs/readers"
9
+ import { pathExists } from "@mailwoman/core/fs/readers/stat"
10
10
  import { GEONAMES_ID_BASE, GEONAMES_POSTAL_ID_BASE } from "@mailwoman/core/resolver/synthetic-id-ranges"
11
11
  import type { DatabaseClient } from "@mailwoman/sqlite/client"
12
12
  import { type PathBuilderLike, resolvePathBuilder } from "path-ts"
@@ -63,7 +63,7 @@ export interface GeonamesIngestProgress {
63
63
  *
64
64
  * It distinguishes a sparse source from aliases that the fold discarded.
65
65
  */
66
- aliasesRefused?: number
66
+ aliasesRefused: number | null
67
67
  }
68
68
 
69
69
  /**
@@ -211,7 +211,7 @@ export async function ingestGeonamesAliases(
211
211
  const file = resolvePathBuilder(geonamesDir, `${cc}.txt`)
212
212
 
213
213
  if (!(await pathExists(file))) {
214
- report({ country: cc, places: 0, skipped: true }, file)
214
+ report({ country: cc, places: 0, skipped: true, aliasesRefused: null }, file)
215
215
 
216
216
  continue
217
217
  }
@@ -24,7 +24,7 @@
24
24
  */
25
25
 
26
26
  import { readUnquotedTSV } from "@mailwoman/core/fs/delimited"
27
- import { pathExists } from "@mailwoman/core/fs/readers"
27
+ import { pathExists } from "@mailwoman/core/fs/readers/stat"
28
28
  import { GEONAMES_POSTAL_ID_BASE } from "@mailwoman/core/resolver/synthetic-id-ranges"
29
29
  import type { DatabaseClient } from "@mailwoman/sqlite/client"
30
30
  import { type PathBuilderLike, resolvePathBuilder } from "path-ts"
package/lib/index.ts CHANGED
@@ -19,7 +19,8 @@ export type {
19
19
  WOFDatabase,
20
20
  } from "#schema"
21
21
 
22
- export { WOFSQLitePlaceLookup, type RankingWeights, type WOFSQLitePlaceLookupOpts } from "#lookup"
22
+ export { WOFSQLitePlaceLookup, type WOFSQLitePlaceLookupOpts } from "#lookup"
23
+ export { type RankingWeights } from "#ranking-weights"
23
24
 
24
25
  export {
25
26
  CANDIDATE_ANCESTOR_COLUMNS,
@@ -98,7 +99,7 @@ export {
98
99
 
99
100
  export { SqliteConventionSource } from "#sqlite/convention-source"
100
101
 
101
- export { WOFPostcodeLookup, type PostcodePlace } from "#postcode-point-lookup"
102
+ export { WOFPostcodeLookup } from "#postcode-point-lookup"
102
103
 
103
104
  export {
104
105
  PLACE_BBOX_TABLE,