@mailwoman/resolver-wof-sqlite 11.0.0 → 11.1.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 (542) hide show
  1. package/README.md +1 -1
  2. package/lib/address/duplicate.ts +133 -0
  3. package/lib/address/keys/attribution.ts +119 -0
  4. package/lib/address/keys/build.ts +768 -0
  5. package/lib/address/keys/layout.ts +166 -0
  6. package/lib/address/keys/local-runner.ts +18 -0
  7. package/lib/address/keys/lookup.ts +205 -0
  8. package/lib/address/keys/open.ts +33 -0
  9. package/lib/address/keys/point-lookup.ts +130 -0
  10. package/lib/address/keys/postcode.ts +27 -0
  11. package/lib/address/keys/schema.ts +264 -0
  12. package/lib/address/number-forms.ts +61 -0
  13. package/lib/address/point/interpolation.ts +28 -23
  14. package/lib/address/point/schema.ts +2 -5
  15. package/lib/address/point/spine-schema.ts +4 -4
  16. package/lib/address/point.ts +12 -79
  17. package/lib/address/record.ts +155 -0
  18. package/lib/address/stack.ts +20 -0
  19. package/lib/address/subdivision-bounds.ts +81 -0
  20. package/lib/address.ts +2 -5
  21. package/lib/ancestry/backfill.ts +5 -4
  22. package/lib/ancestry.ts +10 -10
  23. package/lib/build/candidate.ts +2 -46
  24. package/lib/build/population-corrections.ts +4 -4
  25. package/lib/build/slim.ts +3 -6
  26. package/lib/candidate/alias-bags.ts +4 -4
  27. package/lib/candidate/ancestors/schema.ts +63 -43
  28. package/lib/candidate/ancestors/sidecar.ts +3 -3
  29. package/lib/candidate/bare-admin-names.ts +3 -3
  30. package/lib/candidate/country-display-names.ts +2 -5
  31. package/lib/candidate/extract-fold.ts +2 -5
  32. package/lib/candidate/fts.ts +5 -5
  33. package/lib/candidate/importance.ts +29 -24
  34. package/lib/candidate/lookup.ts +2 -5
  35. package/lib/candidate/name-roles.ts +4 -4
  36. package/lib/candidate/own-name.ts +12 -10
  37. package/lib/candidate/place-attrs.ts +4 -4
  38. package/lib/candidate/schema.ts +2 -5
  39. package/lib/candidate/scoring.ts +3 -6
  40. package/lib/capital-schema.ts +11 -10
  41. package/lib/capitals.ts +15 -12
  42. package/lib/convention/schema.ts +7 -7
  43. package/lib/convention.ts +3 -3
  44. package/lib/coverage-manifest-schema.ts +4 -4
  45. package/lib/env.ts +7 -6
  46. package/lib/exact-match.ts +6 -7
  47. package/lib/extracts.ts +11 -9
  48. package/lib/fst/autocomplete.ts +19 -15
  49. package/lib/fst/binary-matcher.ts +10 -9
  50. package/lib/fst/builder.ts +32 -11
  51. package/lib/fst/deserialize-web.ts +9 -8
  52. package/lib/fst/format.ts +9 -7
  53. package/lib/fst/freshness.ts +3 -4
  54. package/lib/fst/matcher.ts +6 -6
  55. package/lib/fst/serialize.ts +5 -9
  56. package/lib/fst/types.ts +4 -4
  57. package/lib/fst.ts +2 -5
  58. package/lib/fts/query.ts +3 -4
  59. package/lib/fts.ts +2 -5
  60. package/lib/geonames/aliases.ts +193 -43
  61. package/lib/geonames/postal.ts +17 -15
  62. package/lib/geonames.ts +2 -5
  63. package/lib/index.ts +4 -5
  64. package/lib/interpolation.ts +7 -6
  65. package/lib/landmark/rows.ts +5 -5
  66. package/lib/landmark.ts +6 -6
  67. package/lib/lookup.ts +8 -8
  68. package/lib/name-score.ts +7 -6
  69. package/lib/nsul/lookup.ts +19 -15
  70. package/lib/nsul/schema.ts +4 -5
  71. package/lib/nsul.ts +2 -5
  72. package/lib/paths.ts +7 -7
  73. package/lib/place-importance-schema.ts +5 -8
  74. package/lib/poi/lookup.ts +11 -10
  75. package/lib/poi/schema.ts +16 -10
  76. package/lib/poi.ts +2 -5
  77. package/lib/polygon-schema.ts +10 -9
  78. package/lib/postal/city/alias/lookup.ts +14 -13
  79. package/lib/postal/city/alias/schema.ts +12 -12
  80. package/lib/postal/city/candidate-schema.ts +15 -12
  81. package/lib/postal.ts +2 -5
  82. package/lib/postcode-point-lookup.ts +2 -5
  83. package/lib/primary-preference.ts +2 -5
  84. package/lib/proximity-rerank.ts +23 -19
  85. package/lib/ranking-weights.ts +8 -7
  86. package/lib/region-keys.ts +15 -14
  87. package/lib/reverse.ts +16 -17
  88. package/lib/schema.ts +20 -6
  89. package/lib/search-fetch.ts +15 -11
  90. package/lib/sqlite/convention-source.ts +2 -5
  91. package/lib/sqlite/utils.ts +4 -4
  92. package/lib/street/centroid/schema.ts +6 -6
  93. package/lib/street/centroid.ts +3 -3
  94. package/lib/street/locale.ts +64 -0
  95. package/lib/street/morphology/fst/builder.ts +21 -19
  96. package/lib/street/morphology/fst/loader.ts +15 -13
  97. package/lib/street/name-lookup.ts +15 -14
  98. package/lib/street/normalize.ts +20 -17
  99. package/lib/street/segment-schema.ts +16 -14
  100. package/lib/street/table.ts +276 -0
  101. package/lib/street.ts +3 -5
  102. package/lib/types.ts +3 -6
  103. package/lib/unified-schema.ts +31 -9
  104. package/lib/uprn/existence.ts +10 -10
  105. package/lib/uprn/lookup.ts +7 -8
  106. package/lib/uprn/schema.ts +5 -5
  107. package/lib/uprn.ts +2 -5
  108. package/lib/weights-overlay-linker.ts +5 -8
  109. package/out/address/duplicate.d.ts +74 -0
  110. package/out/address/duplicate.d.ts.map +1 -0
  111. package/out/address/duplicate.js +79 -0
  112. package/out/address/duplicate.js.map +1 -0
  113. package/out/address/keys/attribution.d.ts +56 -0
  114. package/out/address/keys/attribution.d.ts.map +1 -0
  115. package/out/address/keys/attribution.js +64 -0
  116. package/out/address/keys/attribution.js.map +1 -0
  117. package/out/address/keys/build.d.ts +154 -0
  118. package/out/address/keys/build.d.ts.map +1 -0
  119. package/out/address/keys/build.js +423 -0
  120. package/out/address/keys/build.js.map +1 -0
  121. package/out/address/keys/layout.d.ts +92 -0
  122. package/out/address/keys/layout.d.ts.map +1 -0
  123. package/out/address/keys/layout.js +87 -0
  124. package/out/address/keys/layout.js.map +1 -0
  125. package/out/address/keys/local-runner.d.ts +9 -0
  126. package/out/address/keys/local-runner.d.ts.map +1 -0
  127. package/out/address/keys/local-runner.js +13 -0
  128. package/out/address/keys/local-runner.js.map +1 -0
  129. package/out/address/keys/lookup.d.ts +80 -0
  130. package/out/address/keys/lookup.d.ts.map +1 -0
  131. package/out/address/keys/lookup.js +104 -0
  132. package/out/address/keys/lookup.js.map +1 -0
  133. package/out/address/keys/open.d.ts +10 -0
  134. package/out/address/keys/open.d.ts.map +1 -0
  135. package/out/address/keys/open.js +28 -0
  136. package/out/address/keys/open.js.map +1 -0
  137. package/out/address/keys/point-lookup.d.ts +25 -0
  138. package/out/address/keys/point-lookup.d.ts.map +1 -0
  139. package/out/address/keys/point-lookup.js +100 -0
  140. package/out/address/keys/point-lookup.js.map +1 -0
  141. package/out/address/keys/postcode.d.ts +9 -0
  142. package/out/address/keys/postcode.d.ts.map +1 -0
  143. package/out/address/keys/postcode.js +25 -0
  144. package/out/address/keys/postcode.js.map +1 -0
  145. package/out/address/keys/schema.d.ts +156 -0
  146. package/out/address/keys/schema.d.ts.map +1 -0
  147. package/out/address/keys/schema.js +119 -0
  148. package/out/address/keys/schema.js.map +1 -0
  149. package/out/address/number-forms.d.ts +19 -0
  150. package/out/address/number-forms.d.ts.map +1 -0
  151. package/out/address/number-forms.js +50 -0
  152. package/out/address/number-forms.js.map +1 -0
  153. package/out/address/point/interpolation.d.ts +24 -22
  154. package/out/address/point/interpolation.d.ts.map +1 -1
  155. package/out/address/point/interpolation.js +3 -31
  156. package/out/address/point/interpolation.js.map +1 -1
  157. package/out/address/point/schema.d.ts +0 -5
  158. package/out/address/point/schema.d.ts.map +1 -1
  159. package/out/address/point/schema.js +2 -5
  160. package/out/address/point/schema.js.map +1 -1
  161. package/out/address/point/spine-schema.d.ts +1 -4
  162. package/out/address/point/spine-schema.d.ts.map +1 -1
  163. package/out/address/point/spine-schema.js +3 -4
  164. package/out/address/point/spine-schema.js.map +1 -1
  165. package/out/address/point.d.ts +0 -3
  166. package/out/address/point.d.ts.map +1 -1
  167. package/out/address/point.js +12 -71
  168. package/out/address/point.js.map +1 -1
  169. package/out/address/record.d.ts +125 -0
  170. package/out/address/record.d.ts.map +1 -0
  171. package/out/address/record.js +49 -0
  172. package/out/address/record.js.map +1 -0
  173. package/out/address/stack.d.ts +14 -0
  174. package/out/address/stack.d.ts.map +1 -0
  175. package/out/address/stack.js +19 -0
  176. package/out/address/stack.js.map +1 -0
  177. package/out/address/subdivision-bounds.d.ts +32 -0
  178. package/out/address/subdivision-bounds.d.ts.map +1 -0
  179. package/out/address/subdivision-bounds.js +48 -0
  180. package/out/address/subdivision-bounds.js.map +1 -0
  181. package/out/address.d.ts +0 -5
  182. package/out/address.d.ts.map +1 -1
  183. package/out/address.js +2 -5
  184. package/out/address.js.map +1 -1
  185. package/out/ancestry/backfill.d.ts +0 -7
  186. package/out/ancestry/backfill.d.ts.map +1 -1
  187. package/out/ancestry/backfill.js +4 -4
  188. package/out/ancestry/backfill.js.map +1 -1
  189. package/out/ancestry.d.ts +0 -14
  190. package/out/ancestry.d.ts.map +1 -1
  191. package/out/ancestry.js +9 -10
  192. package/out/ancestry.js.map +1 -1
  193. package/out/build/candidate.d.ts +0 -22
  194. package/out/build/candidate.d.ts.map +1 -1
  195. package/out/build/candidate.js +2 -26
  196. package/out/build/candidate.js.map +1 -1
  197. package/out/build/population-corrections.d.ts +1 -4
  198. package/out/build/population-corrections.d.ts.map +1 -1
  199. package/out/build/population-corrections.js +2 -19
  200. package/out/build/population-corrections.js.map +1 -1
  201. package/out/build/slim.d.ts +0 -5
  202. package/out/build/slim.d.ts.map +1 -1
  203. package/out/build/slim.js +3 -6
  204. package/out/build/slim.js.map +1 -1
  205. package/out/candidate/alias-bags.d.ts +1 -4
  206. package/out/candidate/alias-bags.d.ts.map +1 -1
  207. package/out/candidate/alias-bags.js +3 -7
  208. package/out/candidate/alias-bags.js.map +1 -1
  209. package/out/candidate/ancestors/schema.d.ts +60 -43
  210. package/out/candidate/ancestors/schema.d.ts.map +1 -1
  211. package/out/candidate/ancestors/schema.js +62 -43
  212. package/out/candidate/ancestors/schema.js.map +1 -1
  213. package/out/candidate/ancestors/sidecar.d.ts +0 -3
  214. package/out/candidate/ancestors/sidecar.d.ts.map +1 -1
  215. package/out/candidate/ancestors/sidecar.js +2 -6
  216. package/out/candidate/ancestors/sidecar.js.map +1 -1
  217. package/out/candidate/bare-admin-names.d.ts +0 -6
  218. package/out/candidate/bare-admin-names.d.ts.map +1 -1
  219. package/out/candidate/bare-admin-names.js +2 -3
  220. package/out/candidate/bare-admin-names.js.map +1 -1
  221. package/out/candidate/country-display-names.d.ts +0 -5
  222. package/out/candidate/country-display-names.d.ts.map +1 -1
  223. package/out/candidate/country-display-names.js +2 -5
  224. package/out/candidate/country-display-names.js.map +1 -1
  225. package/out/candidate/extract-fold.d.ts +0 -5
  226. package/out/candidate/extract-fold.d.ts.map +1 -1
  227. package/out/candidate/extract-fold.js +2 -5
  228. package/out/candidate/extract-fold.js.map +1 -1
  229. package/out/candidate/fts.d.ts +2 -5
  230. package/out/candidate/fts.d.ts.map +1 -1
  231. package/out/candidate/fts.js +2 -7
  232. package/out/candidate/fts.js.map +1 -1
  233. package/out/candidate/importance.d.ts +3 -33
  234. package/out/candidate/importance.d.ts.map +1 -1
  235. package/out/candidate/importance.js +28 -24
  236. package/out/candidate/importance.js.map +1 -1
  237. package/out/candidate/lookup.d.ts +0 -5
  238. package/out/candidate/lookup.d.ts.map +1 -1
  239. package/out/candidate/lookup.js +2 -5
  240. package/out/candidate/lookup.js.map +1 -1
  241. package/out/candidate/name-roles.d.ts +0 -6
  242. package/out/candidate/name-roles.d.ts.map +1 -1
  243. package/out/candidate/name-roles.js +3 -4
  244. package/out/candidate/name-roles.js.map +1 -1
  245. package/out/candidate/own-name.d.ts +0 -18
  246. package/out/candidate/own-name.d.ts.map +1 -1
  247. package/out/candidate/own-name.js +11 -10
  248. package/out/candidate/own-name.js.map +1 -1
  249. package/out/candidate/place-attrs.d.ts +1 -4
  250. package/out/candidate/place-attrs.d.ts.map +1 -1
  251. package/out/candidate/place-attrs.js +2 -12
  252. package/out/candidate/place-attrs.js.map +1 -1
  253. package/out/candidate/schema.d.ts +0 -5
  254. package/out/candidate/schema.d.ts.map +1 -1
  255. package/out/candidate/schema.js +2 -5
  256. package/out/candidate/schema.js.map +1 -1
  257. package/out/candidate/scoring.d.ts +0 -5
  258. package/out/candidate/scoring.d.ts.map +1 -1
  259. package/out/candidate/scoring.js +3 -6
  260. package/out/candidate/scoring.js.map +1 -1
  261. package/out/capital-schema.d.ts +0 -14
  262. package/out/capital-schema.d.ts.map +1 -1
  263. package/out/capital-schema.js +10 -10
  264. package/out/capital-schema.js.map +1 -1
  265. package/out/capitals.d.ts +0 -16
  266. package/out/capitals.d.ts.map +1 -1
  267. package/out/capitals.js +14 -12
  268. package/out/capitals.js.map +1 -1
  269. package/out/convention/schema.d.ts +4 -7
  270. package/out/convention/schema.d.ts.map +1 -1
  271. package/out/convention/schema.js +2 -10
  272. package/out/convention/schema.js.map +1 -1
  273. package/out/convention.d.ts +0 -3
  274. package/out/convention.d.ts.map +1 -1
  275. package/out/convention.js +2 -8
  276. package/out/convention.js.map +1 -1
  277. package/out/coverage-manifest-schema.d.ts +1 -4
  278. package/out/coverage-manifest-schema.d.ts.map +1 -1
  279. package/out/coverage-manifest-schema.js +2 -3
  280. package/out/coverage-manifest-schema.js.map +1 -1
  281. package/out/env.d.ts +0 -8
  282. package/out/env.d.ts.map +1 -1
  283. package/out/env.js +6 -6
  284. package/out/env.js.map +1 -1
  285. package/out/exact-match.d.ts +2 -6
  286. package/out/exact-match.d.ts.map +1 -1
  287. package/out/exact-match.js +3 -9
  288. package/out/exact-match.js.map +1 -1
  289. package/out/extracts.d.ts +8 -9
  290. package/out/extracts.d.ts.map +1 -1
  291. package/out/extracts.js +9 -8
  292. package/out/extracts.js.map +1 -1
  293. package/out/fst/autocomplete.d.ts +1 -19
  294. package/out/fst/autocomplete.d.ts.map +1 -1
  295. package/out/fst/autocomplete.js +2 -18
  296. package/out/fst/autocomplete.js.map +1 -1
  297. package/out/fst/binary-matcher.d.ts +6 -8
  298. package/out/fst/binary-matcher.d.ts.map +1 -1
  299. package/out/fst/binary-matcher.js +3 -13
  300. package/out/fst/binary-matcher.js.map +1 -1
  301. package/out/fst/builder.d.ts +0 -12
  302. package/out/fst/builder.d.ts.map +1 -1
  303. package/out/fst/builder.js +23 -11
  304. package/out/fst/builder.js.map +1 -1
  305. package/out/fst/deserialize-web.d.ts +1 -12
  306. package/out/fst/deserialize-web.d.ts.map +1 -1
  307. package/out/fst/deserialize-web.js +8 -8
  308. package/out/fst/deserialize-web.js.map +1 -1
  309. package/out/fst/format.d.ts +6 -7
  310. package/out/fst/format.d.ts.map +1 -1
  311. package/out/fst/format.js +3 -10
  312. package/out/fst/format.js.map +1 -1
  313. package/out/fst/freshness.d.ts +0 -7
  314. package/out/fst/freshness.d.ts.map +1 -1
  315. package/out/fst/freshness.js +2 -4
  316. package/out/fst/freshness.js.map +1 -1
  317. package/out/fst/matcher.d.ts +3 -6
  318. package/out/fst/matcher.d.ts.map +1 -1
  319. package/out/fst/matcher.js +2 -9
  320. package/out/fst/matcher.js.map +1 -1
  321. package/out/fst/serialize.d.ts +0 -23
  322. package/out/fst/serialize.d.ts.map +1 -1
  323. package/out/fst/serialize.js +4 -8
  324. package/out/fst/serialize.js.map +1 -1
  325. package/out/fst/types.d.ts +1 -4
  326. package/out/fst/types.d.ts.map +1 -1
  327. package/out/fst/types.js +2 -6
  328. package/out/fst/types.js.map +1 -1
  329. package/out/fst.d.ts +0 -5
  330. package/out/fst.d.ts.map +1 -1
  331. package/out/fst.js +2 -5
  332. package/out/fst.js.map +1 -1
  333. package/out/fts/query.d.ts +0 -4
  334. package/out/fts/query.d.ts.map +1 -1
  335. package/out/fts/query.js +2 -7
  336. package/out/fts/query.js.map +1 -1
  337. package/out/fts.d.ts +0 -5
  338. package/out/fts.d.ts.map +1 -1
  339. package/out/fts.js +2 -5
  340. package/out/fts.js.map +1 -1
  341. package/out/geonames/aliases.d.ts +43 -5
  342. package/out/geonames/aliases.d.ts.map +1 -1
  343. package/out/geonames/aliases.js +109 -41
  344. package/out/geonames/aliases.js.map +1 -1
  345. package/out/geonames/postal.d.ts +1 -25
  346. package/out/geonames/postal.d.ts.map +1 -1
  347. package/out/geonames/postal.js +15 -14
  348. package/out/geonames/postal.js.map +1 -1
  349. package/out/geonames.d.ts +0 -5
  350. package/out/geonames.d.ts.map +1 -1
  351. package/out/geonames.js +2 -5
  352. package/out/geonames.js.map +1 -1
  353. package/out/index.d.ts +2 -5
  354. package/out/index.d.ts.map +1 -1
  355. package/out/index.js +4 -5
  356. package/out/index.js.map +1 -1
  357. package/out/interpolation.d.ts +0 -7
  358. package/out/interpolation.d.ts.map +1 -1
  359. package/out/interpolation.js +6 -6
  360. package/out/interpolation.js.map +1 -1
  361. package/out/landmark/rows.d.ts +2 -5
  362. package/out/landmark/rows.d.ts.map +1 -1
  363. package/out/landmark/rows.js +2 -8
  364. package/out/landmark/rows.js.map +1 -1
  365. package/out/landmark.d.ts +3 -6
  366. package/out/landmark.d.ts.map +1 -1
  367. package/out/landmark.js +2 -8
  368. package/out/landmark.js.map +1 -1
  369. package/out/lookup.d.ts +0 -7
  370. package/out/lookup.d.ts.map +1 -1
  371. package/out/lookup.js +7 -8
  372. package/out/lookup.js.map +1 -1
  373. package/out/name-score.d.ts +4 -6
  374. package/out/name-score.d.ts.map +1 -1
  375. package/out/name-score.js +6 -6
  376. package/out/name-score.js.map +1 -1
  377. package/out/nsul/lookup.d.ts +16 -15
  378. package/out/nsul/lookup.d.ts.map +1 -1
  379. package/out/nsul/lookup.js +2 -25
  380. package/out/nsul/lookup.js.map +1 -1
  381. package/out/nsul/schema.d.ts +1 -5
  382. package/out/nsul/schema.d.ts.map +1 -1
  383. package/out/nsul/schema.js +3 -8
  384. package/out/nsul/schema.js.map +1 -1
  385. package/out/nsul.d.ts +0 -5
  386. package/out/nsul.d.ts.map +1 -1
  387. package/out/nsul.js +2 -5
  388. package/out/nsul.js.map +1 -1
  389. package/out/paths.d.ts +1 -13
  390. package/out/paths.d.ts.map +1 -1
  391. package/out/paths.js +6 -7
  392. package/out/paths.js.map +1 -1
  393. package/out/place-importance-schema.d.ts +3 -8
  394. package/out/place-importance-schema.d.ts.map +1 -1
  395. package/out/place-importance-schema.js +3 -6
  396. package/out/place-importance-schema.js.map +1 -1
  397. package/out/poi/lookup.d.ts +0 -13
  398. package/out/poi/lookup.d.ts.map +1 -1
  399. package/out/poi/lookup.js +10 -10
  400. package/out/poi/lookup.js.map +1 -1
  401. package/out/poi/schema.d.ts +13 -10
  402. package/out/poi/schema.d.ts.map +1 -1
  403. package/out/poi/schema.js +2 -13
  404. package/out/poi/schema.js.map +1 -1
  405. package/out/poi.d.ts +0 -5
  406. package/out/poi.d.ts.map +1 -1
  407. package/out/poi.js +2 -5
  408. package/out/poi.js.map +1 -1
  409. package/out/polygon-schema.d.ts +7 -9
  410. package/out/polygon-schema.d.ts.map +1 -1
  411. package/out/polygon-schema.js +2 -12
  412. package/out/polygon-schema.js.map +1 -1
  413. package/out/postal/city/alias/lookup.d.ts +11 -13
  414. package/out/postal/city/alias/lookup.d.ts.map +1 -1
  415. package/out/postal/city/alias/lookup.js +13 -13
  416. package/out/postal/city/alias/lookup.js.map +1 -1
  417. package/out/postal/city/alias/schema.d.ts +9 -12
  418. package/out/postal/city/alias/schema.d.ts.map +1 -1
  419. package/out/postal/city/alias/schema.js +2 -17
  420. package/out/postal/city/alias/schema.js.map +1 -1
  421. package/out/postal/city/candidate-schema.d.ts +12 -12
  422. package/out/postal/city/candidate-schema.d.ts.map +1 -1
  423. package/out/postal/city/candidate-schema.js +14 -12
  424. package/out/postal/city/candidate-schema.js.map +1 -1
  425. package/out/postal.d.ts +0 -5
  426. package/out/postal.d.ts.map +1 -1
  427. package/out/postal.js +2 -5
  428. package/out/postal.js.map +1 -1
  429. package/out/postcode-point-lookup.d.ts +0 -5
  430. package/out/postcode-point-lookup.d.ts.map +1 -1
  431. package/out/postcode-point-lookup.js +2 -5
  432. package/out/postcode-point-lookup.js.map +1 -1
  433. package/out/primary-preference.d.ts +0 -5
  434. package/out/primary-preference.d.ts.map +1 -1
  435. package/out/primary-preference.js +2 -5
  436. package/out/primary-preference.js.map +1 -1
  437. package/out/proximity-rerank.d.ts +2 -26
  438. package/out/proximity-rerank.d.ts.map +1 -1
  439. package/out/proximity-rerank.js +22 -19
  440. package/out/proximity-rerank.js.map +1 -1
  441. package/out/ranking-weights.d.ts +5 -7
  442. package/out/ranking-weights.d.ts.map +1 -1
  443. package/out/ranking-weights.js +2 -9
  444. package/out/ranking-weights.js.map +1 -1
  445. package/out/region-keys.d.ts +0 -15
  446. package/out/region-keys.d.ts.map +1 -1
  447. package/out/region-keys.js +14 -14
  448. package/out/region-keys.js.map +1 -1
  449. package/out/reverse.d.ts +1 -24
  450. package/out/reverse.d.ts.map +1 -1
  451. package/out/reverse.js +14 -16
  452. package/out/reverse.js.map +1 -1
  453. package/out/schema.d.ts +17 -6
  454. package/out/schema.d.ts.map +1 -1
  455. package/out/schema.js +2 -15
  456. package/out/schema.js.map +1 -1
  457. package/out/search-fetch.d.ts +0 -13
  458. package/out/search-fetch.d.ts.map +1 -1
  459. package/out/search-fetch.js +14 -11
  460. package/out/search-fetch.js.map +1 -1
  461. package/out/sqlite/convention-source.d.ts +0 -5
  462. package/out/sqlite/convention-source.d.ts.map +1 -1
  463. package/out/sqlite/convention-source.js +2 -5
  464. package/out/sqlite/convention-source.js.map +1 -1
  465. package/out/sqlite/utils.d.ts +1 -7
  466. package/out/sqlite/utils.d.ts.map +1 -1
  467. package/out/sqlite/utils.js +3 -4
  468. package/out/sqlite/utils.js.map +1 -1
  469. package/out/street/centroid/schema.d.ts +3 -6
  470. package/out/street/centroid/schema.d.ts.map +1 -1
  471. package/out/street/centroid/schema.js +2 -7
  472. package/out/street/centroid/schema.js.map +1 -1
  473. package/out/street/centroid.d.ts +0 -3
  474. package/out/street/centroid.d.ts.map +1 -1
  475. package/out/street/centroid.js +2 -6
  476. package/out/street/centroid.js.map +1 -1
  477. package/out/street/locale.d.ts +12 -0
  478. package/out/street/locale.d.ts.map +1 -0
  479. package/out/street/locale.js +54 -0
  480. package/out/street/locale.js.map +1 -0
  481. package/out/street/morphology/fst/builder.d.ts +1 -26
  482. package/out/street/morphology/fst/builder.d.ts.map +1 -1
  483. package/out/street/morphology/fst/builder.js +19 -18
  484. package/out/street/morphology/fst/builder.js.map +1 -1
  485. package/out/street/morphology/fst/loader.d.ts +0 -20
  486. package/out/street/morphology/fst/loader.d.ts.map +1 -1
  487. package/out/street/morphology/fst/loader.js +14 -13
  488. package/out/street/morphology/fst/loader.js.map +1 -1
  489. package/out/street/name-lookup.d.ts +11 -13
  490. package/out/street/name-lookup.d.ts.map +1 -1
  491. package/out/street/name-lookup.js +14 -14
  492. package/out/street/name-lookup.js.map +1 -1
  493. package/out/street/normalize.d.ts +10 -12
  494. package/out/street/normalize.d.ts.map +1 -1
  495. package/out/street/normalize.js +15 -13
  496. package/out/street/normalize.js.map +1 -1
  497. package/out/street/segment-schema.d.ts +11 -12
  498. package/out/street/segment-schema.d.ts.map +1 -1
  499. package/out/street/segment-schema.js +13 -12
  500. package/out/street/segment-schema.js.map +1 -1
  501. package/out/street/table.d.ts +154 -0
  502. package/out/street/table.d.ts.map +1 -0
  503. package/out/street/table.js +122 -0
  504. package/out/street/table.js.map +1 -0
  505. package/out/street.d.ts +1 -5
  506. package/out/street.d.ts.map +1 -1
  507. package/out/street.js +3 -5
  508. package/out/street.js.map +1 -1
  509. package/out/types.d.ts +1 -6
  510. package/out/types.d.ts.map +1 -1
  511. package/out/types.js +2 -5
  512. package/out/types.js.map +1 -1
  513. package/out/unified-schema.d.ts +18 -7
  514. package/out/unified-schema.d.ts.map +1 -1
  515. package/out/unified-schema.js +14 -10
  516. package/out/unified-schema.js.map +1 -1
  517. package/out/uprn/existence.d.ts +7 -10
  518. package/out/uprn/existence.d.ts.map +1 -1
  519. package/out/uprn/existence.js +9 -10
  520. package/out/uprn/existence.js.map +1 -1
  521. package/out/uprn/lookup.d.ts +1 -8
  522. package/out/uprn/lookup.d.ts.map +1 -1
  523. package/out/uprn/lookup.js +6 -8
  524. package/out/uprn/lookup.js.map +1 -1
  525. package/out/uprn/schema.d.ts +2 -5
  526. package/out/uprn/schema.d.ts.map +1 -1
  527. package/out/uprn/schema.js +2 -7
  528. package/out/uprn/schema.js.map +1 -1
  529. package/out/uprn.d.ts +0 -5
  530. package/out/uprn.d.ts.map +1 -1
  531. package/out/uprn.js +2 -5
  532. package/out/uprn.js.map +1 -1
  533. package/out/weights-overlay-linker.d.ts +3 -8
  534. package/out/weights-overlay-linker.d.ts.map +1 -1
  535. package/out/weights-overlay-linker.js +5 -8
  536. package/out/weights-overlay-linker.js.map +1 -1
  537. package/package.json +12 -12
  538. package/lib/currency-backfill.ts +0 -289
  539. package/out/currency-backfill.d.ts +0 -68
  540. package/out/currency-backfill.d.ts.map +0 -1
  541. package/out/currency-backfill.js +0 -169
  542. package/out/currency-backfill.js.map +0 -1
@@ -0,0 +1,768 @@
1
+ // SPDX-FileCopyrightText: Teffen Ellis and Mailwoman contributors
2
+ // SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
3
+
4
+ /**
5
+ * Builds one region's address layer, `lookup.db` and `detail.db`, from a stream of address records.
6
+ *
7
+ * Records are staged once into a scratch database with one row per key they answer.
8
+ * Each key table is then written in key order with `INSERT … SELECT … ORDER BY`,
9
+ * so its B-tree pages hold contiguous keys.
10
+ */
11
+
12
+ import { ByteFormatter } from "@mailwoman/core/fs/formatters"
13
+ import { readFilesystemSpace, statPath } from "@mailwoman/core/fs/readers/stat"
14
+ import { makeDirectories, removePathIfPresent } from "@mailwoman/core/fs/writers"
15
+ import { parseJSONStrict, stringifyJSON } from "@mailwoman/core/json"
16
+ import { sourcePresentCoverageCells } from "@mailwoman/core/layers"
17
+ import { type LayerManifest, writeLayerCoverage, writeLayerManifest } from "@mailwoman/core/layers/manifest"
18
+ import { createLayerCoverageTable, createLayerManifestTable } from "@mailwoman/core/layers/schema"
19
+ import { shortCellToInt } from "@mailwoman/spatial"
20
+ import { DatabaseClient } from "@mailwoman/sqlite/client"
21
+ import { buildSealedArtifact } from "@mailwoman/sqlite/sealed/build"
22
+ import { latLngToCell } from "@sister.software/h3"
23
+ import { type Kysely, sql } from "kysely"
24
+ import { basename, dirname, resolvePath, type PathBuilderLike } from "path-ts"
25
+
26
+ import {
27
+ ADDRESS_DUPLICATE_STACK_LIMIT,
28
+ type AddressDuplicateRule,
29
+ type DuplicateCandidate,
30
+ isDuplicateAddress,
31
+ isNearbyDuplicateAddress,
32
+ } from "#address/duplicate"
33
+ import { addressPostcodeKey } from "#address/keys/postcode"
34
+ import {
35
+ ADDRESS_SCOPED_KEY_TABLES,
36
+ ADDRESS_STREET_TABLES,
37
+ type AddressDetailDatabase,
38
+ type AddressLookupDatabase,
39
+ createAddressDetailTable,
40
+ createAddressPlaceKeyTable,
41
+ createAddressRegionKeyTable,
42
+ createAddressScopedKeyTable,
43
+ createAddressSourceTable,
44
+ createAddressStreetTable,
45
+ } from "#address/keys/schema"
46
+ import type { AddressRecord } from "#address/record"
47
+ import { addressPointStacksSQL } from "#address/stack"
48
+
49
+ /**
50
+ * The SQLite page size of both files.
51
+ *
52
+ * At 8 KiB a key hit read 4 pages in 3 range chunks on the Wisconsin prototype.
53
+ */
54
+ export const ADDRESS_LAYER_PAGE_SIZE = 8192
55
+
56
+ /**
57
+ * The H3 resolution `layer_coverage` counts points at, about 36 km² per cell.
58
+ */
59
+ export const ADDRESS_LAYER_COVERAGE_RESOLUTION = 6
60
+
61
+ /**
62
+ * The provenance a caller states for the layer it builds.
63
+ *
64
+ * The build supplies the schema version, the spine keys and the per-source record counts.
65
+ */
66
+ export type AddressLayerManifest = Omit<LayerManifest, "schemaVersion" | "spineKeys" | "sourceRecords">
67
+
68
+ /**
69
+ * The level each staged key row answers.
70
+ * Each level is written to the key table of the same name.
71
+ */
72
+ const StagedLevel = {
73
+ Postcode: "key_postcode",
74
+ Locality: "key_locality",
75
+ Subregion: "key_subregion",
76
+ Region: "key_region",
77
+ } as const
78
+
79
+ type StagedLevel = (typeof StagedLevel)[keyof typeof StagedLevel]
80
+
81
+ /**
82
+ * The scratch schema: every key row an address answers, and every address's detail row.
83
+ */
84
+ interface StagingDatabase extends AddressDetailDatabase {
85
+ stage: {
86
+ level: StagedLevel
87
+ scope: string
88
+ street: string
89
+ number: string
90
+ address_id: number
91
+ lat: number
92
+ lon: number
93
+ source_id: number
94
+ }
95
+ source_ids: {
96
+ name: string
97
+ source_id: number
98
+ }
99
+ }
100
+
101
+ /**
102
+ * One keyed read a build must answer: a street and number under one level's scope.
103
+ *
104
+ * `scope` is ignored for `key_region`, which has none.
105
+ */
106
+ export interface AddressLayerReadCheck {
107
+ table: StagedLevel
108
+ scope: string
109
+ street: string
110
+ number: string
111
+ }
112
+
113
+ /**
114
+ * What a source's rows are published under, from the register.
115
+ */
116
+ export interface AddressSourceTerms {
117
+ license: string | null
118
+ authority: string | null
119
+ }
120
+
121
+ export interface BuildAddressLayerOptions {
122
+ records: AsyncIterable<AddressRecord>
123
+ /**
124
+ * The terms of each source id the records hold.
125
+ * A record whose source has no entry stops the build.
126
+ */
127
+ sources: ReadonlyMap<string, AddressSourceTerms>
128
+ /**
129
+ * The `layer_manifest` row `lookup.db` holds.
130
+ */
131
+ manifest: AddressLayerManifest
132
+ /**
133
+ * Keyed reads the finished `lookup.db` must answer before it is sealed,
134
+ * such as a known address at each level.
135
+ */
136
+ checks?: readonly AddressLayerReadCheck[]
137
+ lookupPath: PathBuilderLike
138
+ detailPath: PathBuilderLike
139
+ /**
140
+ * Where the staging database is written, the largest file a build makes.
141
+ *
142
+ * It defaults to beside `lookupPath`.
143
+ * A directory on another disk keeps that file off the layer's disk.
144
+ */
145
+ stagingDirectory?: PathBuilderLike
146
+ /**
147
+ * Helper threads SQLite may use for each large sort: the staged key groups
148
+ * and every key table's `INSERT … SELECT … ORDER BY`.
149
+ *
150
+ * SQLite starts each connection with none and caps the count at its compile-time
151
+ * `SQLITE_MAX_WORKER_THREADS`, 8 in Node's build.
152
+ * The sorted output is the same with any count.
153
+ *
154
+ * @defaultValue 0
155
+ */
156
+ sortThreads?: number
157
+ /**
158
+ * Called after every millionth record with the count staged so far.
159
+ */
160
+ onProgress?: (records: number) => void
161
+ /**
162
+ * Returns why a record is left out of the layer, such as `outside-subdivision`, or `null` to keep it.
163
+ *
164
+ * Each refusal is counted in the summary under its source and reason.
165
+ */
166
+ refuseRecord?: (record: AddressRecord) => string | null
167
+ /**
168
+ * The most distinct street-and-number pairs one source may place at one point, rounded to 1e-5 degrees.
169
+ *
170
+ * The records at a point over this limit are left out under the reason `stacked-point`.
171
+ *
172
+ * @defaultValue {@linkcode ADDRESS_LAYER_STACK_LIMIT}
173
+ */
174
+ stackLimit?: number
175
+ /**
176
+ * Drops each record that repeats a higher-ranked source's record of the same postcode key,
177
+ * by {@link isDuplicateAddress}, under the reason `duplicate`.
178
+ * Without it every source's record is kept.
179
+ */
180
+ duplicates?: AddressDuplicateRule
181
+ }
182
+
183
+ /**
184
+ * The most distinct addresses one source may place at one point before the layer refuses them all.
185
+ *
186
+ * Across the 2026-11-08 US layer, point counts fall bin by bin from 2 to 1,000 addresses per point,
187
+ * and four points lie above 1,000: two hold 1,004 and 1,653 addresses, and two hold 10,525 and 12,868.
188
+ */
189
+ export const ADDRESS_LAYER_STACK_LIMIT = 1000
190
+
191
+ /**
192
+ * The counts one build wrote.
193
+ * Its checks compare them against the tables.
194
+ */
195
+ export interface AddressLayerBuildSummary {
196
+ records: number
197
+ /**
198
+ * Key rows written per key table.
199
+ */
200
+ keys: Record<StagedLevel, number>
201
+ /**
202
+ * Records per source id.
203
+ */
204
+ sources: Record<string, number>
205
+ /**
206
+ * Locality names per record, the duplication the per-form keys cost.
207
+ */
208
+ localityFormsPerRecord: number
209
+ /**
210
+ * Points per H3 cell at {@link ADDRESS_LAYER_COVERAGE_RESOLUTION}, keyed by the cell's short integer form.
211
+ */
212
+ coverage: ReadonlyMap<number, number>
213
+ /**
214
+ * Records left out of the layer, per source id and reason.
215
+ */
216
+ refused: Record<string, Record<string, number>>
217
+ }
218
+
219
+ async function createStagingTables(db: Kysely<StagingDatabase>): Promise<void> {
220
+ await db.schema
221
+ .createTable("stage")
222
+ .addColumn("level", "text", (c) => c.notNull())
223
+ .addColumn("scope", "text", (c) => c.notNull())
224
+ .addColumn("street", "text", (c) => c.notNull())
225
+ .addColumn("number", "text", (c) => c.notNull())
226
+ .addColumn("address_id", "integer", (c) => c.notNull())
227
+ .addColumn("lat", "real", (c) => c.notNull())
228
+ .addColumn("lon", "real", (c) => c.notNull())
229
+ .addColumn("source_id", "integer", (c) => c.notNull())
230
+ .execute()
231
+
232
+ await db.schema
233
+ .createTable("source_ids")
234
+ .addColumn("name", "text", (c) => c.notNull())
235
+ .addColumn("source_id", "integer", (c) => c.notNull())
236
+ .execute()
237
+
238
+ await createAddressDetailTable(db)
239
+ }
240
+
241
+ /**
242
+ * Stages every record, then writes `lookup.db` and `detail.db` beside their targets
243
+ * and swaps each into place.
244
+ *
245
+ * @throws When a record's source has no entry in `sources`, or when a key table's
246
+ * row count differs from the rows staged for its level.
247
+ */
248
+ export async function buildAddressLayer(options: BuildAddressLayerOptions): Promise<AddressLayerBuildSummary> {
249
+ const stagingName = `${basename(options.lookupPath.toString())}.stage-${process.pid}`
250
+
251
+ const stagingPath = options.stagingDirectory
252
+ ? resolvePath(options.stagingDirectory, stagingName)
253
+ : `${options.lookupPath}.stage-${process.pid}`
254
+
255
+ if (options.stagingDirectory) {
256
+ await makeDirectories(options.stagingDirectory)
257
+ }
258
+
259
+ await Promise.all([makeDirectories(dirname(options.lookupPath)), makeDirectories(dirname(options.detailPath))])
260
+ await removePathIfPresent(stagingPath)
261
+
262
+ try {
263
+ const summary = await stageRecords(options, stagingPath)
264
+ const threads = sortThreadsPragma(options.sortThreads)
265
+
266
+ await assertOutputSpace(options.lookupPath, stagingPath)
267
+
268
+ await writeLookup(
269
+ options.lookupPath,
270
+ stagingPath,
271
+ summary,
272
+ options.sources,
273
+ options.manifest,
274
+ options.checks ?? [],
275
+ threads
276
+ )
277
+
278
+ await writeDetail(options.detailPath, stagingPath, summary, threads)
279
+
280
+ return summary
281
+ } finally {
282
+ await removePathIfPresent(stagingPath)
283
+ }
284
+ }
285
+
286
+ /**
287
+ * The `PRAGMA threads` statement for `count` sorter helper threads, a whole number from 0.
288
+ */
289
+ function sortThreadsPragma(count: number | undefined): string {
290
+ return `PRAGMA threads = ${Math.max(0, Math.floor(count ?? 0))}`
291
+ }
292
+
293
+ /**
294
+ * Throws before `lookup.db` and `detail.db` are written when their filesystem has
295
+ * less free space than the staging database occupies.
296
+ *
297
+ * The staging database holds every row both files are written from, so its size bounds what they need.
298
+ * A shortfall found here stops the build with the space it needs.
299
+ *
300
+ * Without the check, SQLite reports `database or disk is full` partway through writing.
301
+ */
302
+ async function assertOutputSpace(lookupPath: PathBuilderLike, stagingPath: string): Promise<void> {
303
+ const needed = (await statPath(stagingPath)).size
304
+ const space = await readFilesystemSpace(dirname(lookupPath))
305
+
306
+ if (space.availableBytes < needed) {
307
+ throw new Error(
308
+ `${space.measuredAt} has ${ByteFormatter.formatIEC(space.availableBytes)} free, and writing ` +
309
+ `${basename(lookupPath.toString())} and detail.db from the staging database ${stagingPath} needs up to ` +
310
+ `${ByteFormatter.formatIEC(needed)}. Free space on that filesystem and build again.`
311
+ )
312
+ }
313
+ }
314
+
315
+ async function stageRecords(options: BuildAddressLayerOptions, stagingPath: string): Promise<AddressLayerBuildSummary> {
316
+ using db = new DatabaseClient<StagingDatabase>(stagingPath)
317
+
318
+ db.exec(
319
+ `PRAGMA page_size = ${ADDRESS_LAYER_PAGE_SIZE}; PRAGMA journal_mode = OFF; PRAGMA synchronous = OFF; PRAGMA cache_size = -400000;`
320
+ )
321
+
322
+ db.exec(sortThreadsPragma(options.sortThreads))
323
+
324
+ await createStagingTables(db)
325
+
326
+ const stage = db.prepare("INSERT INTO stage VALUES (?, ?, ?, ?, ?, ?, ?, ?)")
327
+ const detail = db.prepare("INSERT INTO address VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)")
328
+ const sourceIDs = new Map<string, number>()
329
+ const keys: Record<StagedLevel, number> = { key_postcode: 0, key_locality: 0, key_subregion: 0, key_region: 0 }
330
+ const sources: Record<string, number> = {}
331
+ const coverage = new Map<number, number>()
332
+ const refused: Record<string, Record<string, number>> = {}
333
+ let records = 0
334
+ let localityNames = 0
335
+
336
+ const refuse = (source: string, reason: string, count = 1) => {
337
+ const bySource = (refused[source] ??= {})
338
+
339
+ bySource[reason] = (bySource[reason] ?? 0) + count
340
+ }
341
+
342
+ db.exec("BEGIN")
343
+
344
+ for await (const record of options.records) {
345
+ if (!options.sources.has(record.source)) {
346
+ throw new Error(
347
+ `address record ${record.sourceRecordID} has source ${record.source}, which the build lists no terms for`
348
+ )
349
+ }
350
+
351
+ const reason = options.refuseRecord?.(record)
352
+
353
+ if (reason) {
354
+ refuse(record.source, reason)
355
+
356
+ continue
357
+ }
358
+
359
+ let sourceID = sourceIDs.get(record.source)
360
+
361
+ if (sourceID == null) {
362
+ sourceID = sourceIDs.size + 1
363
+ sourceIDs.set(record.source, sourceID)
364
+ }
365
+
366
+ const addressID = ++records
367
+
368
+ sources[record.source] = (sources[record.source] ?? 0) + 1
369
+
370
+ const put = (level: StagedLevel, scope: string) => {
371
+ stage.run(level, scope, record.streetKey, record.number, addressID, record.lat, record.lon, sourceID)
372
+
373
+ keys[level]++
374
+ }
375
+
376
+ const postcode = addressPostcodeKey(record.country, record.postcode)
377
+
378
+ if (postcode) {
379
+ put(StagedLevel.Postcode, postcode)
380
+ }
381
+
382
+ for (const name of record.names) {
383
+ if (name.level === "locality" || name.level === "dependent_locality") {
384
+ put(StagedLevel.Locality, name.key)
385
+
386
+ localityNames++
387
+ } else if (name.level === "subregion") {
388
+ put(StagedLevel.Subregion, name.key)
389
+ }
390
+ }
391
+
392
+ put(StagedLevel.Region, "")
393
+
394
+ const cell = shortCellToInt(latLngToCell(record.lat, record.lon, ADDRESS_LAYER_COVERAGE_RESOLUTION))
395
+
396
+ coverage.set(cell, (coverage.get(cell) ?? 0) + 1)
397
+
398
+ detail.run(
399
+ addressID,
400
+ sourceID,
401
+ record.sourceRecordID,
402
+ record.country,
403
+ record.number,
404
+ record.unit,
405
+ record.streetRaw,
406
+ record.postcode,
407
+ stringifyJSON(record.names.map(({ level, form, value }) => ({ level, form, value }))),
408
+ record.adminCode,
409
+ record.lat,
410
+ record.lon,
411
+ record.placement,
412
+ record.authority
413
+ )
414
+
415
+ if (records % 200_000 === 0) {
416
+ db.exec("COMMIT; BEGIN")
417
+ }
418
+
419
+ if (records % 1_000_000 === 0) {
420
+ options.onProgress?.(records)
421
+ }
422
+ }
423
+
424
+ db.exec("COMMIT")
425
+
426
+ const sourceRow = db.prepare("INSERT INTO source_ids VALUES (?, ?)")
427
+
428
+ for (const [name, id] of sourceIDs) {
429
+ sourceRow.run(name, id)
430
+ }
431
+
432
+ const sourceNames = new Map([...sourceIDs].map(([name, id]) => [id, name]))
433
+
434
+ // Removes the records a temporary table lists from the staged keys and details,
435
+ // counts them under `reason`, and lowers the counts the build's checks compare to match.
436
+ // The rows are streamed: one state can drop tens of millions of repeats,
437
+ // more than fit in the heap as objects.
438
+ const drop = (table: string, reason: string) => {
439
+ let dropped = 0
440
+
441
+ for (const row of db
442
+ .prepare(`SELECT a.source_id, a.lat, a.lon FROM ${table} t JOIN address a USING (address_id)`)
443
+ .iterate() as Iterable<{
444
+ source_id: number
445
+ lat: number
446
+ lon: number
447
+ }>) {
448
+ dropped++
449
+
450
+ const source = sourceNames.get(row.source_id)!
451
+ const cell = shortCellToInt(latLngToCell(row.lat, row.lon, ADDRESS_LAYER_COVERAGE_RESOLUTION))
452
+ const remaining = (coverage.get(cell) ?? 0) - 1
453
+
454
+ if (remaining > 0) {
455
+ coverage.set(cell, remaining)
456
+ } else {
457
+ coverage.delete(cell)
458
+ }
459
+
460
+ sources[source] = (sources[source] ?? 0) - 1
461
+ refuse(source, reason)
462
+ }
463
+
464
+ for (const { level, n } of db
465
+ .prepare(
466
+ `SELECT level, count(*) AS n FROM stage WHERE address_id IN (SELECT address_id FROM ${table}) GROUP BY level`
467
+ )
468
+ .all() as Array<{ level: StagedLevel; n: number }>) {
469
+ keys[level] -= n
470
+ }
471
+
472
+ db.exec(`DELETE FROM stage WHERE address_id IN (SELECT address_id FROM ${table})`)
473
+ db.exec(`DELETE FROM address WHERE address_id IN (SELECT address_id FROM ${table})`)
474
+ db.exec(`DROP TABLE ${table}`)
475
+ records -= dropped
476
+ }
477
+
478
+ // A point one source shares among more than `stackLimit` addresses locates none of them.
479
+ db.exec(`CREATE TEMP TABLE stacked AS
480
+ SELECT address_id FROM address
481
+ WHERE (source_id, round(lat, 5), round(lon, 5)) IN (
482
+ SELECT source_id, la, lo FROM (${addressPointStacksSQL(options.stackLimit ?? ADDRESS_LAYER_STACK_LIMIT)}))`)
483
+
484
+ drop("stacked", "stacked-point")
485
+
486
+ if (options.duplicates) {
487
+ markDuplicates(db, options.duplicates, sourceNames)
488
+ drop("duplicate", "duplicate")
489
+ }
490
+
491
+ return {
492
+ records,
493
+ keys,
494
+ sources,
495
+ localityFormsPerRecord: records ? localityNames / records : 0,
496
+ coverage,
497
+ refused,
498
+ }
499
+ }
500
+
501
+ /**
502
+ * Lists in a temporary `duplicate` table every staged record that repeats a higher-ranked
503
+ * record of its postcode key group, by {@link isDuplicateAddress}.
504
+ * Only groups holding two or more sources are read.
505
+ */
506
+ function markDuplicates(
507
+ db: DatabaseClient<StagingDatabase>,
508
+ rule: AddressDuplicateRule,
509
+ sourceNames: ReadonlyMap<number, string>
510
+ ): void {
511
+ // SQLite rounds the points for both the stack count and the join, so a record meets its own point's count.
512
+ db.exec(`CREATE TEMP TABLE stack AS ${addressPointStacksSQL(ADDRESS_DUPLICATE_STACK_LIMIT)}`)
513
+
514
+ db.exec("CREATE INDEX temp.stack_point ON stack (source_id, la, lo)")
515
+ db.exec("CREATE TEMP TABLE duplicate (address_id INTEGER PRIMARY KEY)")
516
+
517
+ const mark = db.prepare("INSERT OR IGNORE INTO duplicate VALUES (?)")
518
+
519
+ type Member = DuplicateCandidate & { addressID: number; rank: number; postcode: boolean }
520
+
521
+ // Reads each street-and-number group the query yields, in key order, and marks every
522
+ // lower-ranked member that `repeats` judges a repeat of a higher-ranked one.
523
+ const settleGroups = (query: string, repeats: (higher: Member, lower: Member) => boolean) => {
524
+ let group: Member[] = []
525
+ let key = ""
526
+
527
+ const settle = () => {
528
+ for (const lower of group) {
529
+ if (group.some((higher) => higher.rank < lower.rank && repeats(higher, lower))) {
530
+ mark.run(lower.addressID)
531
+ }
532
+ }
533
+ }
534
+
535
+ for (const row of db.prepare(query).iterate() as Iterable<{
536
+ scope: string
537
+ street: string
538
+ key_number: string
539
+ address_id: number
540
+ source_id: number
541
+ unit: string | null
542
+ number: string
543
+ street_raw: string
544
+ names: string
545
+ lat: number
546
+ lon: number
547
+ stack: number
548
+ postcode: number
549
+ }>) {
550
+ const rowKey = `${row.scope}|${row.street}|${row.key_number}`
551
+
552
+ if (rowKey !== key) {
553
+ settle()
554
+ group = []
555
+ key = rowKey
556
+ }
557
+
558
+ const source = sourceNames.get(row.source_id)!
559
+ const names = parseJSONStrict<Array<{ level: string; value: string }>>(row.names)
560
+
561
+ group.push({
562
+ addressID: row.address_id,
563
+ rank: rule.rank(source),
564
+ source,
565
+ unit: row.unit,
566
+ number: row.number,
567
+ streetRaw: row.street_raw,
568
+ localities: names.filter((name) => name.level === "locality").map((name) => name.value),
569
+ lat: row.lat,
570
+ lon: row.lon,
571
+ stack: row.stack,
572
+ postcode: row.postcode === 1,
573
+ })
574
+ }
575
+
576
+ settle()
577
+ }
578
+
579
+ const memberColumns = `s.scope, s.street, s.number AS key_number, a.address_id, a.source_id, a.unit, a.number,
580
+ a.street_raw, a.names, a.lat, a.lon, coalesce(k.n, 1) AS stack`
581
+
582
+ const stackJoin =
583
+ "LEFT JOIN stack k ON k.source_id = a.source_id AND k.la = round(a.lat, 5) AND k.lo = round(a.lon, 5)"
584
+
585
+ db.exec("BEGIN")
586
+
587
+ settleGroups(
588
+ `SELECT ${memberColumns}, 1 AS postcode
589
+ FROM stage s JOIN address a ON a.address_id = s.address_id ${stackJoin}
590
+ WHERE s.level = '${StagedLevel.Postcode}' AND (s.scope, s.street, s.number) IN (
591
+ SELECT scope, street, number FROM stage WHERE level = '${StagedLevel.Postcode}'
592
+ GROUP BY 1, 2, 3 HAVING count(DISTINCT source_id) > 1)
593
+ ORDER BY s.scope, s.street, s.number`,
594
+ (higher, lower) => isDuplicateAddress(rule, higher, lower)
595
+ )
596
+
597
+ // A record with no postcode key shares no postcode group, so it is compared at the region level
598
+ // with every record of its street and number, and repeats one only when their points are near.
599
+ db.exec(
600
+ `CREATE TEMP TABLE keyed (address_id INTEGER PRIMARY KEY);
601
+ INSERT OR IGNORE INTO keyed SELECT address_id FROM stage WHERE level = '${StagedLevel.Postcode}'`
602
+ )
603
+
604
+ settleGroups(
605
+ `SELECT ${memberColumns}, p.address_id IS NOT NULL AS postcode
606
+ FROM stage s JOIN address a ON a.address_id = s.address_id ${stackJoin}
607
+ LEFT JOIN keyed p ON p.address_id = s.address_id
608
+ WHERE s.level = '${StagedLevel.Region}' AND (s.street, s.number) IN (
609
+ SELECT g.street, g.number FROM stage g LEFT JOIN keyed q ON q.address_id = g.address_id
610
+ WHERE g.level = '${StagedLevel.Region}'
611
+ GROUP BY 1, 2 HAVING count(DISTINCT g.source_id) > 1 AND count(q.address_id) < count(*))
612
+ ORDER BY s.street, s.number`,
613
+ (higher, lower) => !(higher.postcode && lower.postcode) && isNearbyDuplicateAddress(rule, higher, lower)
614
+ )
615
+
616
+ db.exec("DROP TABLE keyed")
617
+ db.exec("COMMIT")
618
+ db.exec("DROP TABLE stack")
619
+ }
620
+
621
+ async function writeLookup(
622
+ lookupPath: PathBuilderLike,
623
+ stagingPath: string,
624
+ summary: AddressLayerBuildSummary,
625
+ terms: ReadonlyMap<string, AddressSourceTerms>,
626
+ manifest: AddressLayerManifest,
627
+ checks: readonly AddressLayerReadCheck[],
628
+ threads: string
629
+ ): Promise<void> {
630
+ await buildSealedArtifact<AddressLookupDatabase, true, void>({
631
+ out: lookupPath,
632
+ createTables: async (db) => {
633
+ db.exec(`PRAGMA page_size = ${ADDRESS_LAYER_PAGE_SIZE}`)
634
+ db.exec(threads)
635
+ await createLayerManifestTable(db)
636
+ await createLayerCoverageTable(db)
637
+ await createAddressSourceTable(db)
638
+
639
+ for (const table of ADDRESS_SCOPED_KEY_TABLES) {
640
+ await createAddressScopedKeyTable(db, table)
641
+ }
642
+
643
+ await createAddressPlaceKeyTable(db)
644
+ await createAddressRegionKeyTable(db)
645
+
646
+ for (const table of ADDRESS_STREET_TABLES) {
647
+ await createAddressStreetTable(db, table)
648
+ }
649
+ },
650
+ ingest: async (db) => {
651
+ db.exec(`ATTACH DATABASE '${stagingPath.replaceAll("'", "''")}' AS staged`)
652
+
653
+ const insertSource = db.prepare("INSERT INTO source VALUES (?, ?, ?, ?, ?)")
654
+
655
+ const ids = db.prepare("SELECT name, source_id FROM staged.source_ids ORDER BY source_id").all() as Array<{
656
+ name: string
657
+ source_id: number
658
+ }>
659
+
660
+ for (const { name, source_id } of ids) {
661
+ const term = terms.get(name)!
662
+
663
+ insertSource.run(source_id, name, term.license, term.authority, summary.sources[name] ?? 0)
664
+ }
665
+
666
+ // A record's locality and dependent-locality names can share a key,
667
+ // so a key row repeated for one address is written once.
668
+ for (const table of ADDRESS_SCOPED_KEY_TABLES) {
669
+ db.exec(
670
+ `INSERT OR IGNORE INTO ${table} SELECT scope, street, number, address_id, lat, lon, source_id
671
+ FROM staged.stage WHERE level = '${table}' ORDER BY scope, street, number, address_id`
672
+ )
673
+ }
674
+
675
+ db.exec(
676
+ `INSERT OR IGNORE INTO key_region SELECT street, number, address_id, lat, lon, source_id
677
+ FROM staged.stage WHERE level = 'key_region' ORDER BY street, number, address_id`
678
+ )
679
+
680
+ for (const [table, level] of [
681
+ ["street_postcode", "key_postcode"],
682
+ ["street_locality", "key_locality"],
683
+ ] as const) {
684
+ db.exec(
685
+ `INSERT INTO ${table} SELECT scope, street, avg(lat), avg(lon), min(lat), max(lat), min(lon), max(lon), count(*)
686
+ FROM staged.stage WHERE level = '${level}' GROUP BY scope, street ORDER BY scope, street`
687
+ )
688
+ }
689
+
690
+ db.exec("DETACH DATABASE staged")
691
+
692
+ return true
693
+ },
694
+ finish: async (db) => {
695
+ for (const table of [...ADDRESS_SCOPED_KEY_TABLES, "key_region"] as const) {
696
+ const { count } = (await db
697
+ .selectFrom(table)
698
+ .select(sql<number>`count(*)`.as("count"))
699
+ .executeTakeFirstOrThrow()) as { count: number }
700
+
701
+ if (count > summary.keys[table]) {
702
+ throw new Error(`${table} holds ${count} rows, more than the ${summary.keys[table]} staged for it`)
703
+ }
704
+ }
705
+
706
+ const unanswered = checks.filter((check) => {
707
+ const row =
708
+ check.table === StagedLevel.Region
709
+ ? db
710
+ .prepare("SELECT 1 FROM key_region WHERE street = ? AND number = ? LIMIT 1")
711
+ .get(check.street, check.number)
712
+ : db
713
+ .prepare(`SELECT 1 FROM ${check.table} WHERE scope = ? AND street = ? AND number = ? LIMIT 1`)
714
+ .get(check.scope, check.street, check.number)
715
+
716
+ return row == null
717
+ })
718
+
719
+ if (unanswered.length) {
720
+ const listed = unanswered.map((check) => `${check.table} ${check.scope} ${check.street} ${check.number}`)
721
+
722
+ throw new Error(`lookup.db misses ${unanswered.length} of ${checks.length} checked reads: ${listed.join("; ")}`)
723
+ }
724
+
725
+ await writeLayerCoverage(db, sourcePresentCoverageCells(summary.coverage))
726
+
727
+ await writeLayerManifest(db, {
728
+ ...manifest,
729
+ schemaVersion: 1,
730
+ spineKeys: { h3: { column: "layer_coverage.h3_cell", resolution: ADDRESS_LAYER_COVERAGE_RESOLUTION } },
731
+ sourceRecords: summary.sources,
732
+ })
733
+ },
734
+ })
735
+ }
736
+
737
+ async function writeDetail(
738
+ detailPath: PathBuilderLike,
739
+ stagingPath: string,
740
+ summary: AddressLayerBuildSummary,
741
+ threads: string
742
+ ) {
743
+ await buildSealedArtifact<AddressDetailDatabase, true, void>({
744
+ out: detailPath,
745
+ createTables: async (db) => {
746
+ db.exec(`PRAGMA page_size = ${ADDRESS_LAYER_PAGE_SIZE}`)
747
+ db.exec(threads)
748
+ await createAddressDetailTable(db)
749
+ },
750
+ ingest: async (db) => {
751
+ db.exec(`ATTACH DATABASE '${stagingPath.replaceAll("'", "''")}' AS staged`)
752
+ db.exec("INSERT INTO address SELECT * FROM staged.address ORDER BY address_id")
753
+ db.exec("DETACH DATABASE staged")
754
+
755
+ return true
756
+ },
757
+ finish: async (db) => {
758
+ const { count } = (await db
759
+ .selectFrom("address")
760
+ .select(sql<number>`count(*)`.as("count"))
761
+ .executeTakeFirstOrThrow()) as { count: number }
762
+
763
+ if (count !== summary.records) {
764
+ throw new Error(`address holds ${count} rows, against ${summary.records} records staged`)
765
+ }
766
+ },
767
+ })
768
+ }