@mailwoman/resolver-wof-sqlite 8.0.0 → 8.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (219) hide show
  1. package/address-point-interpolation.ts +9 -3
  2. package/address-point-schema.ts +32 -10
  3. package/address-point.ts +3 -0
  4. package/ancestry-backfill.ts +18 -5
  5. package/ancestry.ts +7 -2
  6. package/build-candidate.ts +29 -6
  7. package/build-slim.ts +40 -11
  8. package/candidate-fts.ts +1 -0
  9. package/candidate-lookup.ts +49 -15
  10. package/candidate-schema.ts +35 -11
  11. package/coincident-roles.ts +28 -6
  12. package/convention.ts +3 -1
  13. package/coverage-manifest-schema.ts +243 -0
  14. package/fst-autocomplete.ts +15 -9
  15. package/fst-builder.ts +65 -9
  16. package/fst-deserialize-web.ts +46 -9
  17. package/fst-matcher.ts +12 -5
  18. package/fst-serialize.ts +83 -9
  19. package/fst-types.ts +43 -0
  20. package/fts-query.ts +84 -0
  21. package/fts.ts +35 -9
  22. package/geo.ts +9 -3
  23. package/geonames-aliases.ts +116 -79
  24. package/geonames-postal.ts +25 -5
  25. package/index.ts +19 -0
  26. package/interpolation.ts +59 -55
  27. package/lookup.ts +103 -292
  28. package/name-score.ts +76 -0
  29. package/out/address-point-interpolation.d.ts.map +1 -1
  30. package/out/address-point-interpolation.js +4 -2
  31. package/out/address-point-interpolation.js.map +1 -1
  32. package/out/address-point-schema.d.ts +30 -10
  33. package/out/address-point-schema.d.ts.map +1 -1
  34. package/out/address-point-schema.js +6 -2
  35. package/out/address-point-schema.js.map +1 -1
  36. package/out/address-point.d.ts.map +1 -1
  37. package/out/address-point.js.map +1 -1
  38. package/out/ancestry-backfill.d.ts +6 -2
  39. package/out/ancestry-backfill.d.ts.map +1 -1
  40. package/out/ancestry-backfill.js +7 -3
  41. package/out/ancestry-backfill.js.map +1 -1
  42. package/out/ancestry.d.ts +6 -2
  43. package/out/ancestry.d.ts.map +1 -1
  44. package/out/ancestry.js +3 -1
  45. package/out/ancestry.js.map +1 -1
  46. package/out/build-candidate.d.ts +9 -3
  47. package/out/build-candidate.d.ts.map +1 -1
  48. package/out/build-candidate.js +5 -3
  49. package/out/build-candidate.js.map +1 -1
  50. package/out/build-slim.d.ts +15 -5
  51. package/out/build-slim.d.ts.map +1 -1
  52. package/out/build-slim.js +11 -5
  53. package/out/build-slim.js.map +1 -1
  54. package/out/candidate-fts.d.ts.map +1 -1
  55. package/out/candidate-fts.js.map +1 -1
  56. package/out/candidate-lookup.d.ts +16 -3
  57. package/out/candidate-lookup.d.ts.map +1 -1
  58. package/out/candidate-lookup.js +27 -11
  59. package/out/candidate-lookup.js.map +1 -1
  60. package/out/candidate-schema.d.ts +33 -11
  61. package/out/candidate-schema.d.ts.map +1 -1
  62. package/out/candidate-schema.js.map +1 -1
  63. package/out/coincident-roles.d.ts +16 -4
  64. package/out/coincident-roles.d.ts.map +1 -1
  65. package/out/coincident-roles.js +9 -3
  66. package/out/coincident-roles.js.map +1 -1
  67. package/out/convention.d.ts +3 -1
  68. package/out/convention.d.ts.map +1 -1
  69. package/out/convention.js.map +1 -1
  70. package/out/coverage-manifest-schema.d.ts +112 -0
  71. package/out/coverage-manifest-schema.d.ts.map +1 -0
  72. package/out/coverage-manifest-schema.js +154 -0
  73. package/out/coverage-manifest-schema.js.map +1 -0
  74. package/out/fst-autocomplete.d.ts +1 -1
  75. package/out/fst-autocomplete.d.ts.map +1 -1
  76. package/out/fst-autocomplete.js +11 -9
  77. package/out/fst-autocomplete.js.map +1 -1
  78. package/out/fst-builder.d.ts.map +1 -1
  79. package/out/fst-builder.js +42 -9
  80. package/out/fst-builder.js.map +1 -1
  81. package/out/fst-deserialize-web.d.ts.map +1 -1
  82. package/out/fst-deserialize-web.js +34 -9
  83. package/out/fst-deserialize-web.js.map +1 -1
  84. package/out/fst-matcher.d.ts +6 -2
  85. package/out/fst-matcher.d.ts.map +1 -1
  86. package/out/fst-matcher.js +9 -5
  87. package/out/fst-matcher.js.map +1 -1
  88. package/out/fst-serialize.d.ts.map +1 -1
  89. package/out/fst-serialize.js +62 -9
  90. package/out/fst-serialize.js.map +1 -1
  91. package/out/fst-types.d.ts +43 -0
  92. package/out/fst-types.d.ts.map +1 -1
  93. package/out/fts-query.d.ts +41 -0
  94. package/out/fts-query.d.ts.map +1 -0
  95. package/out/fts-query.js +75 -0
  96. package/out/fts-query.js.map +1 -0
  97. package/out/fts.d.ts +21 -7
  98. package/out/fts.d.ts.map +1 -1
  99. package/out/fts.js +10 -4
  100. package/out/fts.js.map +1 -1
  101. package/out/geo.d.ts +6 -2
  102. package/out/geo.d.ts.map +1 -1
  103. package/out/geo.js +3 -1
  104. package/out/geo.js.map +1 -1
  105. package/out/geonames-aliases.d.ts +12 -4
  106. package/out/geonames-aliases.d.ts.map +1 -1
  107. package/out/geonames-aliases.js +72 -67
  108. package/out/geonames-aliases.js.map +1 -1
  109. package/out/geonames-postal.d.ts +9 -3
  110. package/out/geonames-postal.d.ts.map +1 -1
  111. package/out/geonames-postal.js +7 -2
  112. package/out/geonames-postal.js.map +1 -1
  113. package/out/index.d.ts +2 -0
  114. package/out/index.d.ts.map +1 -1
  115. package/out/index.js +1 -0
  116. package/out/index.js.map +1 -1
  117. package/out/interpolation.d.ts +24 -6
  118. package/out/interpolation.d.ts.map +1 -1
  119. package/out/interpolation.js +32 -40
  120. package/out/interpolation.js.map +1 -1
  121. package/out/lookup.d.ts +3 -97
  122. package/out/lookup.d.ts.map +1 -1
  123. package/out/lookup.js +52 -184
  124. package/out/lookup.js.map +1 -1
  125. package/out/name-score.d.ts +28 -0
  126. package/out/name-score.d.ts.map +1 -0
  127. package/out/name-score.js +67 -0
  128. package/out/name-score.js.map +1 -0
  129. package/out/poi-lookup.d.ts +24 -8
  130. package/out/poi-lookup.d.ts.map +1 -1
  131. package/out/poi-lookup.js +27 -13
  132. package/out/poi-lookup.js.map +1 -1
  133. package/out/poi-schema.d.ts +42 -13
  134. package/out/poi-schema.d.ts.map +1 -1
  135. package/out/poi-schema.js +12 -3
  136. package/out/poi-schema.js.map +1 -1
  137. package/out/postal-city-alias-lookup.d.ts +18 -6
  138. package/out/postal-city-alias-lookup.d.ts.map +1 -1
  139. package/out/postal-city-alias-lookup.js.map +1 -1
  140. package/out/postal-city-alias-schema.d.ts +27 -9
  141. package/out/postal-city-alias-schema.d.ts.map +1 -1
  142. package/out/postal-city-alias-schema.js +3 -1
  143. package/out/postal-city-alias-schema.js.map +1 -1
  144. package/out/postal-city-candidate-schema.d.ts +15 -5
  145. package/out/postal-city-candidate-schema.d.ts.map +1 -1
  146. package/out/postal-city-candidate-schema.js +3 -1
  147. package/out/postal-city-candidate-schema.js.map +1 -1
  148. package/out/postcode-point-lookup.d.ts +6 -2
  149. package/out/postcode-point-lookup.d.ts.map +1 -1
  150. package/out/postcode-point-lookup.js +6 -2
  151. package/out/postcode-point-lookup.js.map +1 -1
  152. package/out/ranking-weights.d.ts +118 -0
  153. package/out/ranking-weights.d.ts.map +1 -0
  154. package/out/ranking-weights.js +44 -0
  155. package/out/ranking-weights.js.map +1 -0
  156. package/out/reverse.d.ts +9 -3
  157. package/out/reverse.d.ts.map +1 -1
  158. package/out/reverse.js +20 -6
  159. package/out/reverse.js.map +1 -1
  160. package/out/sharding.d.ts +3 -1
  161. package/out/sharding.d.ts.map +1 -1
  162. package/out/sharding.js +7 -5
  163. package/out/sharding.js.map +1 -1
  164. package/out/sqlite-convention-source.d.ts.map +1 -1
  165. package/out/sqlite-convention-source.js +3 -1
  166. package/out/sqlite-convention-source.js.map +1 -1
  167. package/out/street-centroid-schema.d.ts +33 -11
  168. package/out/street-centroid-schema.d.ts.map +1 -1
  169. package/out/street-centroid-schema.js +3 -1
  170. package/out/street-centroid-schema.js.map +1 -1
  171. package/out/street-centroid.d.ts.map +1 -1
  172. package/out/street-centroid.js +6 -2
  173. package/out/street-centroid.js.map +1 -1
  174. package/out/street-morphology-fst-builder.d.ts +6 -2
  175. package/out/street-morphology-fst-builder.d.ts.map +1 -1
  176. package/out/street-morphology-fst-builder.js +8 -7
  177. package/out/street-morphology-fst-builder.js.map +1 -1
  178. package/out/street-morphology-fst-loader.d.ts +67 -0
  179. package/out/street-morphology-fst-loader.d.ts.map +1 -0
  180. package/out/street-morphology-fst-loader.js +59 -0
  181. package/out/street-morphology-fst-loader.js.map +1 -0
  182. package/out/street-name-lookup.d.ts +9 -3
  183. package/out/street-name-lookup.d.ts.map +1 -1
  184. package/out/street-name-lookup.js +9 -7
  185. package/out/street-name-lookup.js.map +1 -1
  186. package/out/street-normalize.d.ts +3 -1
  187. package/out/street-normalize.d.ts.map +1 -1
  188. package/out/street-normalize.js +23 -13
  189. package/out/street-normalize.js.map +1 -1
  190. package/out/street-segment-schema.d.ts +68 -13
  191. package/out/street-segment-schema.d.ts.map +1 -1
  192. package/out/street-segment-schema.js +21 -2
  193. package/out/street-segment-schema.js.map +1 -1
  194. package/out/types.d.ts +18 -6
  195. package/out/types.d.ts.map +1 -1
  196. package/out/unified-schema.d.ts +1 -1
  197. package/out/unified-schema.d.ts.map +1 -1
  198. package/out/unified-schema.js +2 -2
  199. package/out/unified-schema.js.map +1 -1
  200. package/package.json +13 -5
  201. package/poi-lookup.ts +53 -21
  202. package/poi-schema.ts +43 -13
  203. package/postal-city-alias-lookup.ts +20 -6
  204. package/postal-city-alias-schema.ts +28 -9
  205. package/postal-city-candidate-schema.ts +15 -5
  206. package/postcode-point-lookup.ts +6 -2
  207. package/ranking-weights.ts +148 -0
  208. package/reverse.ts +47 -10
  209. package/sharding.ts +13 -6
  210. package/sqlite-convention-source.ts +4 -1
  211. package/street-centroid-schema.ts +35 -11
  212. package/street-centroid.ts +10 -3
  213. package/street-morphology-fst-builder.ts +25 -9
  214. package/street-morphology-fst-loader.ts +103 -0
  215. package/street-name-lookup.ts +19 -7
  216. package/street-normalize.ts +28 -13
  217. package/street-segment-schema.ts +83 -13
  218. package/types.ts +18 -6
  219. package/unified-schema.ts +11 -2
@@ -39,9 +39,13 @@ function hasColumn(db: DatabaseSync, table: string, column: string): boolean {
39
39
  }
40
40
 
41
41
  export interface SQLiteStreetNameLookupOpts {
42
- /** ISO-2 (upper-case) countries this index answers for. Default `["FR"]` (the BAN street-centroids instance). */
42
+ /**
43
+ * ISO-2 (upper-case) countries this index answers for. Default `["FR"]` (the BAN street-centroids instance).
44
+ */
43
45
  countries?: Iterable<string>
44
- /** Table name. Default `street_centroid`. */
46
+ /**
47
+ * Table name. Default `street_centroid`.
48
+ */
45
49
  table?: string
46
50
  }
47
51
 
@@ -68,9 +72,11 @@ export class SQLiteStreetNameLookup implements StreetLocalityEvidence {
68
72
  // `name_key` MUST match `foldStreetSurface` here (the fold-parity contract).
69
73
  const keyCol = hasColumn(this.#db, table, "name_key") ? "name_key" : "street_norm"
70
74
  this.#byName = this.#db.prepare(`SELECT 1 FROM ${table} WHERE ${keyCol} = ? LIMIT 1`)
75
+
71
76
  this.#byNameLocality = this.#db.prepare(
72
77
  `SELECT 1 FROM ${table} WHERE ${keyCol} = ? AND locality_base = ? LIMIT 1`
73
78
  )
79
+
74
80
  this.#byNamePostcode = this.#db.prepare(`SELECT 1 FROM ${table} WHERE ${keyCol} = ? AND postcode = ? LIMIT 1`)
75
81
  }
76
82
  }
@@ -83,18 +89,24 @@ export class SQLiteStreetNameLookup implements StreetLocalityEvidence {
83
89
 
84
90
  // Scoped lookups tighten precision when the hypothesis carries a locality/postcode; a scoped MISS falls back to the
85
91
  // unscoped probe (index incompleteness in the scope column is not evidence of absence — positive-evidence rule).
86
- if (scope?.locality && this.#byNameLocality) {
87
- if (this.#byNameLocality.get(norm, foldStreetSurface(scope.locality)) !== undefined) return true
92
+ if (
93
+ scope?.locality &&
94
+ this.#byNameLocality &&
95
+ this.#byNameLocality.get(norm, foldStreetSurface(scope.locality)) !== undefined
96
+ ) {
97
+ return true
88
98
  }
89
99
 
90
- if (scope?.postcode && this.#byNamePostcode) {
91
- if (this.#byNamePostcode.get(norm, scope.postcode) !== undefined) return true
100
+ if (scope?.postcode && this.#byNamePostcode && this.#byNamePostcode.get(norm, scope.postcode) !== undefined) {
101
+ return true
92
102
  }
93
103
 
94
104
  return this.#byName.get(norm) !== undefined
95
105
  }
96
106
 
97
- /** Close the underlying handle. */
107
+ /**
108
+ * Close the underlying handle.
109
+ */
98
110
  close(): void {
99
111
  this.#db.close()
100
112
  }
@@ -24,6 +24,12 @@
24
24
 
25
25
  import { AbbreviationToDirectional, US_STREET_SUFFIX_LOOKUP } from "@mailwoman/codex/us"
26
26
 
27
+ /**
28
+ * Token count a street must exceed before its trailing pair is merged. At or below it the pair IS the whole street
29
+ * name, and merging would leave nothing to match on.
30
+ */
31
+ const MIN_TOKENS_FOR_TAIL_MERGE = 3
32
+
27
33
  /**
28
34
  * Spelled ordinal street names → their digit-ordinal form ("tenth" → "10th"), applied ONLY when a street-type suffix
29
35
  * follows (#723 admin-tail) — so the ordinal cross-streets common in grid cities ("Tenth Street", "Fifth Avenue") match
@@ -63,14 +69,16 @@ const SPELLED_ORDINAL_TO_DIGIT = new Map<string, string>([
63
69
  ["hundredth", "100th"],
64
70
  ])
65
71
 
66
- /** Lowercase + diacritic-fold + punctuation strip + whitespace collapse. */
72
+ /**
73
+ * Lowercase + diacritic-fold + punctuation strip + whitespace collapse.
74
+ */
67
75
  function fold(input: string): string {
68
76
  return input
69
77
  .normalize("NFKD")
70
- .replace(/[̀-ͯ]/g, "")
78
+ .replaceAll(/[̀-ͯ]/g, "")
71
79
  .toLowerCase()
72
- .replace(/[.,'’]/g, "")
73
- .replace(/\s+/g, " ")
80
+ .replaceAll(/[.,'’]/g, "")
81
+ .replaceAll(/\s+/g, " ")
74
82
  .trim()
75
83
  }
76
84
 
@@ -81,7 +89,7 @@ function fold(input: string): string {
81
89
  export function normalizeStreetForKey(street: string): string {
82
90
  const tokens = fold(street).split(" ")
83
91
 
84
- if (tokens.length === 0) return ""
92
+ if (!tokens.length) return ""
85
93
 
86
94
  // Spelled-ordinal street names → digit form when a street suffix follows ("Tenth Street" →
87
95
  // "10th street", #723). Gated on the next token being a suffix so ordinal-WORD names are untouched.
@@ -99,6 +107,7 @@ export function normalizeStreetForKey(street: string): string {
99
107
  // ("southeast"), and also merge an already-written two-token pair ("South East …").
100
108
  const edgeDirectional = (raw: string) =>
101
109
  AbbreviationToDirectional.get(raw.toUpperCase())?.toLowerCase().replace(" ", "")
110
+
102
111
  const mergePair = (a?: string, b?: string) =>
103
112
  a && b && /^(north|south)$/.test(a) && /^(east|west)$/.test(b) ? a + b : undefined
104
113
 
@@ -107,20 +116,21 @@ export function normalizeStreetForKey(street: string): string {
107
116
  if (leadPair && tokens.length > 2) {
108
117
  tokens.splice(0, 2, leadPair)
109
118
  }
119
+
110
120
  const first = edgeDirectional(tokens[0]!)
111
121
 
112
122
  if (first && tokens.length > 1) {
113
123
  tokens[0] = first
114
124
  }
115
125
 
116
- const tailPair = mergePair(tokens[tokens.length - 2], tokens[tokens.length - 1])
126
+ const tailPair = mergePair(tokens.at(-2), tokens.at(-1))
117
127
 
118
- if (tailPair && tokens.length > 3) {
119
- tokens.splice(tokens.length - 2, 2, tailPair)
128
+ if (tailPair && tokens.length > MIN_TOKENS_FOR_TAIL_MERGE) {
129
+ tokens.splice(-2, 2, tailPair)
120
130
  }
121
131
 
122
132
  if (tokens.length > 2) {
123
- const last = edgeDirectional(tokens[tokens.length - 1]!)
133
+ const last = edgeDirectional(tokens.at(-1)!)
124
134
 
125
135
  if (last) {
126
136
  tokens[tokens.length - 1] = last
@@ -136,6 +146,7 @@ export function normalizeStreetForKey(street: string): string {
136
146
 
137
147
  if (canonical) {
138
148
  tokens[at] = canonical.toLowerCase()
149
+
139
150
  break
140
151
  }
141
152
  }
@@ -196,9 +207,9 @@ export function normalizeStreetForKeyLocale(street: string, locale: StreetLocale
196
207
  // hyphen ("Champs-Élysées", "St-Honoré") or a space — both sides fold identically, so this is pure
197
208
  // robustness. It also splits a hyphenated abbreviation ("St-Honoré" → "st honore") into tokens the
198
209
  // per-locale type/Saint map can see.
199
- const tokens = fold(street).replace(/ß/g, "ss").replace(/-/g, " ").split(/\s+/).filter(Boolean)
210
+ const tokens = fold(street).replaceAll("ß", "ss").replaceAll("-", " ").split(/\s+/).filter(Boolean)
200
211
 
201
- if (tokens.length === 0) return ""
212
+ if (!tokens.length) return ""
202
213
 
203
214
  switch (locale) {
204
215
  case "fr":
@@ -229,7 +240,9 @@ export function normalizeStreetForKeyLocale(street: string, locale: StreetLocale
229
240
  return tokens.join(" ")
230
241
  }
231
242
 
232
- /** Normalize a locality name for address-point keying (fold only — no street semantics). */
243
+ /**
244
+ * Normalize a locality name for address-point keying (fold only — no street semantics).
245
+ */
233
246
  export function normalizeLocalityForKey(locality: string): string {
234
247
  return fold(locality)
235
248
  }
@@ -268,7 +281,9 @@ export function stripLocalityQualifier(locality: string): string {
268
281
 
269
282
  if (s.includes("/")) {
270
283
  s = s.split("/")[0]!.trim()
271
- } // "Kraubath/Mur", "St.Kanzian/Klopeiner See"
284
+ }
285
+
286
+ // "Kraubath/Mur", "St.Kanzian/Klopeiner See"
272
287
  s = s.replace(/\s+[a-zà-ÿ]\.\s*\S.*$/iu, "") // abbreviated " b.Graz" / " o.Bleiburg" / " a.d. …"
273
288
  s = s.replace(/\s+(im|an der|ob|bei|in der|unter|vor)\s+\S.*$/iu, "") // " im Simmental", " bei Graz"
274
289
  s = s.replace(/\s+(S|N|E|W|V|Ø|Sø|Fyn|Thy|Sjælland|Jylland|[A-ZÅÄÖ]{2})$/u, "") // " S", " VD", " Thy"
@@ -23,34 +23,79 @@ import type { Kysely } from "kysely"
23
23
  * `odd`/`even`/`mixed`.
24
24
  */
25
25
  export interface StreetSegmentTable {
26
- /** Shared {@link normalizeStreetForKey} of the street — the build/query-consistent probe key. */
26
+ /**
27
+ * Shared {@link normalizeStreetForKey} of the street — the build/query-consistent probe key.
28
+ */
27
29
  street_norm: string
28
- /** `L` or `R` — the TIGER side the address range sits on. */
30
+ /**
31
+ * `L` or `R` — the TIGER side the address range sits on.
32
+ */
29
33
  side: string
30
34
  from_hn: number
31
35
  to_hn: number
32
- /** Sorted lower bound of `(from_hn, to_hn)` — the probe filters `min_hn <= n <= max_hn`. */
36
+ /**
37
+ * Sorted lower bound of `(from_hn, to_hn)` — the probe filters `min_hn <= n <= max_hn`.
38
+ */
33
39
  min_hn: number
34
- /** Sorted upper bound of `(from_hn, to_hn)`. */
40
+ /**
41
+ * Sorted upper bound of `(from_hn, to_hn)`.
42
+ */
35
43
  max_hn: number
36
- /** `odd` | `even` | `mixed` — the house-number parity along the range. */
44
+ /**
45
+ * `odd` | `even` | `mixed` — the house-number parity along the range.
46
+ */
37
47
  parity: string
38
48
  postcode: string | null
39
- /** 5-digit state+county FIPS the edge came from. */
49
+ /**
50
+ * 5-digit state+county FIPS the edge came from.
51
+ */
40
52
  county_fips: string
41
- /** The street as it appeared in TIGER (kept for display / debugging). */
53
+ /**
54
+ * The street as it appeared in TIGER (kept for display / debugging).
55
+ */
42
56
  street_raw: string
43
- /** GeoJSON LineString text (no SpatiaLite — read back with `JSON.parse`). */
57
+ /**
58
+ * GeoJSON LineString text (no SpatiaLite — read back with `JSON.parse`).
59
+ */
44
60
  geometry: string
45
- /** Provenance: the dataset this edge came from (e.g. `tiger:edges`). */
61
+ /**
62
+ * Provenance: the dataset this edge came from (e.g. `tiger:edges`).
63
+ */
46
64
  source: string
47
- /** The pinned TIGER release the edge was ingested from. */
65
+ /**
66
+ * The pinned TIGER release the edge was ingested from.
67
+ */
48
68
  release: string
49
69
  }
50
70
 
51
- /** The street-segment database schema for `new DatabaseClient<StreetSegmentDatabase>(...)`. */
71
+ /**
72
+ * The shard's single-row calibration metadata (#374 doctrine, 2026-07-26): the conformal radius multiplier is a
73
+ * property of the CALIBRATION SET the artifact was built against — so it ships IN the artifact (the pair-index δ
74
+ * precedent, `neural/pair-index-resolver.ts`), not in caller code. Written once by the builder; read at open time by
75
+ * {@link StreetInterpolator}. Shards built before this table exists simply lack it — the reader degrades to `undefined`
76
+ * and callers fall back to the in-code per-region table (never patch shipped DBs — rebuild).
77
+ */
78
+ export interface InterpCalibrationRow {
79
+ /**
80
+ * Conformal multiplier for the raw half-segment `uncertainty_m` radius (#374/#584) — ×Q̂ for a ~90% bound.
81
+ */
82
+ radius_multiplier: number
83
+ /**
84
+ * Provenance of the multiplier (e.g. `split-conformal:2026-06-14`).
85
+ */
86
+ method: string
87
+ /**
88
+ * The calibration-table key the multiplier was selected by (a USPS region code, or `default` for unmeasured).
89
+ */
90
+ region: string
91
+ }
92
+
93
+ /**
94
+ * The street-segment database schema for `new DatabaseClient<StreetSegmentDatabase>(...)`.
95
+ */
52
96
  export interface StreetSegmentDatabase {
53
97
  street_segment: StreetSegmentTable
98
+ interp_calibration: InterpCalibrationRow
54
99
  }
55
100
 
56
101
  /**
@@ -73,7 +118,9 @@ export const STREET_SEGMENT_COLUMNS = [
73
118
  "release",
74
119
  ] as const
75
120
 
76
- /** Create the `street_segment` table — called before the streaming bulk load. */
121
+ /**
122
+ * Create the `street_segment` table — called before the streaming bulk load.
123
+ */
77
124
  export async function createStreetSegmentTable(db: Kysely<StreetSegmentDatabase>): Promise<void> {
78
125
  await db.schema
79
126
  .createTable("street_segment")
@@ -93,12 +140,35 @@ export async function createStreetSegmentTable(db: Kysely<StreetSegmentDatabase>
93
140
  .execute()
94
141
  }
95
142
 
96
- /** Create the two probe indexes the reader relies on (postcode-scope, street-scope). */
143
+ /**
144
+ * Create + populate the single-row `interp_calibration` metadata table (see {@link InterpCalibrationRow}) — called once
145
+ * by the shard builder, after the value is selected from the calibration source of record. Build-time only (async
146
+ * Kysely is fine here); the READ side is the raw sync probe in {@link StreetInterpolator}'s constructor, per the
147
+ * sync-by-interface doctrine.
148
+ */
149
+ export async function writeInterpCalibration(
150
+ db: Kysely<StreetSegmentDatabase>,
151
+ row: InterpCalibrationRow
152
+ ): Promise<void> {
153
+ await db.schema
154
+ .createTable("interp_calibration")
155
+ .addColumn("radius_multiplier", "real", (c) => c.notNull())
156
+ .addColumn("method", "text", (c) => c.notNull())
157
+ .addColumn("region", "text", (c) => c.notNull())
158
+ .execute()
159
+
160
+ await db.insertInto("interp_calibration").values(row).execute()
161
+ }
162
+
163
+ /**
164
+ * Create the two probe indexes the reader relies on (postcode-scope, street-scope).
165
+ */
97
166
  export async function createStreetSegmentIndexes(db: Kysely<StreetSegmentDatabase>): Promise<void> {
98
167
  await db.schema
99
168
  .createIndex("idx_seg_postcode")
100
169
  .on("street_segment")
101
170
  .columns(["postcode", "street_norm", "min_hn"])
102
171
  .execute()
172
+
103
173
  await db.schema.createIndex("idx_seg_street").on("street_segment").columns(["street_norm", "min_hn"]).execute()
104
174
  }
package/types.ts CHANGED
@@ -49,7 +49,9 @@ export interface PlaceCandidate {
49
49
  id: number
50
50
  name: string
51
51
  placetype: WOFPlacetype
52
- /** ISO 3166-1 alpha-2 country code. */
52
+ /**
53
+ * ISO 3166-1 alpha-2 country code.
54
+ */
53
55
  country: string
54
56
  lat: number
55
57
  lon: number
@@ -125,9 +127,13 @@ export interface GeoBbox {
125
127
  export interface FindPlaceQuery {
126
128
  text: string
127
129
  placetype?: WOFPlacetype | WOFPlacetype[]
128
- /** ISO 3166-1 alpha-2 — narrows to one country. */
130
+ /**
131
+ * ISO 3166-1 alpha-2 — narrows to one country.
132
+ */
129
133
  country?: string
130
- /** WOF place id — narrows to descendants of this place. */
134
+ /**
135
+ * WOF place id — narrows to descendants of this place.
136
+ */
131
137
  parentID?: number
132
138
  /**
133
139
  * Sibling postcode. When set on a `locality` query AND a `postcode_locality` table is present, triggers the
@@ -136,7 +142,9 @@ export interface FindPlaceQuery {
136
142
  * postcode_locality shard is present.
137
143
  */
138
144
  postcode?: string
139
- /** Proximity hint — candidates close to this point get a ranking boost. */
145
+ /**
146
+ * Proximity hint — candidates close to this point get a ranking boost.
147
+ */
140
148
  near?: GeoPoint & { maxDistanceKm?: number }
141
149
  /**
142
150
  * Ordered proximity-bias points (viewport center, user location, …), each optionally weighted (default 1.0, first
@@ -147,9 +155,13 @@ export interface FindPlaceQuery {
147
155
  * back-compat.
148
156
  */
149
157
  bias?: Array<GeoPoint & { weight?: number }>
150
- /** Bounding-box filter — only candidates whose bbox intersects this box are returned. */
158
+ /**
159
+ * Bounding-box filter — only candidates whose bbox intersects this box are returned.
160
+ */
151
161
  bbox?: GeoBbox
152
- /** Default 10. */
162
+ /**
163
+ * Default 10.
164
+ */
153
165
  limit?: number
154
166
  }
155
167
 
package/unified-schema.ts CHANGED
@@ -12,7 +12,7 @@
12
12
  * `place_search` FTS5 + `place_bbox` R*Tree are built separately by `build-fts` (fts.ts).
13
13
  */
14
14
 
15
- import { DatabaseSync } from "node:sqlite"
15
+ import type { DatabaseSync } from "node:sqlite"
16
16
 
17
17
  import { DatabaseClient } from "@mailwoman/core/kysley/client"
18
18
 
@@ -109,11 +109,13 @@ export async function createUnifiedSchema(db: DatabaseSync): Promise<void> {
109
109
  */
110
110
  export function populateAncestors(db: DatabaseSync): number {
111
111
  db.exec("DELETE FROM ancestors")
112
+
112
113
  const rows = db.prepare("SELECT id, parent_id, placetype FROM spr").all() as Array<{
113
114
  id: number
114
115
  parent_id: number
115
116
  placetype: string
116
117
  }>
118
+
117
119
  const byID = new Map<number, { parent: number; placetype: string }>()
118
120
 
119
121
  for (const r of rows) {
@@ -125,7 +127,9 @@ export function populateAncestors(db: DatabaseSync): number {
125
127
  let count = 0
126
128
 
127
129
  for (const r of rows) {
128
- insert.run(r.id, r.id, r.placetype) // self
130
+ insert.run(r.id, r.id, r.placetype)
131
+
132
+ // self
129
133
  count++
130
134
  const seen = new Set<number>([r.id])
131
135
  let cur = r.parent_id
@@ -135,11 +139,13 @@ export function populateAncestors(db: DatabaseSync): number {
135
139
 
136
140
  if (!node) break
137
141
  insert.run(r.id, cur, node.placetype)
142
+
138
143
  count++
139
144
  seen.add(cur)
140
145
  cur = node.parent
141
146
  }
142
147
  }
148
+
143
149
  db.exec("COMMIT")
144
150
 
145
151
  return count
@@ -152,18 +158,21 @@ export async function createUnifiedIndexes(db: DatabaseSync): Promise<void> {
152
158
  await kdb.schema.createIndex("spr_by_parent").ifNotExists().on("spr").column("parent_id").execute()
153
159
  await kdb.schema.createIndex("names_by_id").ifNotExists().on("names").column("id").execute()
154
160
  await kdb.schema.createIndex("names_by_name").ifNotExists().on("names").column("name").execute()
161
+
155
162
  await kdb.schema
156
163
  .createIndex("concordances_by_id")
157
164
  .ifNotExists()
158
165
  .on("concordances")
159
166
  .columns(["id", "lastmodified"])
160
167
  .execute()
168
+
161
169
  await kdb.schema
162
170
  .createIndex("concordances_by_other_id")
163
171
  .ifNotExists()
164
172
  .on("concordances")
165
173
  .columns(["other_source", "other_id"])
166
174
  .execute()
175
+
167
176
  // ancestor_id is the hot column (parent-constraint queries `WHERE ancestor_id = ?`); id supports
168
177
  // the reverse lookup.
169
178
  await kdb.schema.createIndex("ancestors_by_ancestor").ifNotExists().on("ancestors").column("ancestor_id").execute()