@mailwoman/corpus 8.5.0 → 9.0.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 (310) hide show
  1. package/README.md +1 -1
  2. package/out/src/adapter.d.ts +41 -0
  3. package/out/src/adapter.d.ts.map +1 -1
  4. package/out/src/adapter.js +46 -1
  5. package/out/src/adapter.js.map +1 -1
  6. package/out/src/adapters/ban/adapter.d.ts +4 -3
  7. package/out/src/adapters/ban/adapter.d.ts.map +1 -1
  8. package/out/src/adapters/ban/adapter.js +60 -67
  9. package/out/src/adapters/ban/adapter.js.map +1 -1
  10. package/out/src/adapters/ban/street-decompose.d.ts.map +1 -1
  11. package/out/src/adapters/ban/street-decompose.js +18 -25
  12. package/out/src/adapters/ban/street-decompose.js.map +1 -1
  13. package/out/src/adapters/fcc-bdc/adapter.d.ts +0 -6
  14. package/out/src/adapters/fcc-bdc/adapter.d.ts.map +1 -1
  15. package/out/src/adapters/fcc-bdc/adapter.js +2 -24
  16. package/out/src/adapters/fcc-bdc/adapter.js.map +1 -1
  17. package/out/src/adapters/geonames/adapter.d.ts.map +1 -1
  18. package/out/src/adapters/geonames/adapter.js +70 -76
  19. package/out/src/adapters/geonames/adapter.js.map +1 -1
  20. package/out/src/adapters/geonames-postal/adapter.d.ts.map +1 -1
  21. package/out/src/adapters/geonames-postal/adapter.js +44 -49
  22. package/out/src/adapters/geonames-postal/adapter.js.map +1 -1
  23. package/out/src/adapters/gnaf/adapter.d.ts.map +1 -1
  24. package/out/src/adapters/gnaf/adapter.js +4 -7
  25. package/out/src/adapters/gnaf/adapter.js.map +1 -1
  26. package/out/src/adapters/gnaf/assemble.d.ts.map +1 -1
  27. package/out/src/adapters/gnaf/assemble.js +6 -11
  28. package/out/src/adapters/gnaf/assemble.js.map +1 -1
  29. package/out/src/adapters/openaddresses/adapter.d.ts.map +1 -1
  30. package/out/src/adapters/openaddresses/adapter.js +2 -7
  31. package/out/src/adapters/openaddresses/adapter.js.map +1 -1
  32. package/out/src/adapters/overture/adapter.d.ts.map +1 -1
  33. package/out/src/adapters/overture/adapter.js +3 -7
  34. package/out/src/adapters/overture/adapter.js.map +1 -1
  35. package/out/src/adapters/state-hi-schools/adapter.d.ts.map +1 -1
  36. package/out/src/adapters/state-hi-schools/adapter.js +55 -73
  37. package/out/src/adapters/state-hi-schools/adapter.js.map +1 -1
  38. package/out/src/adapters/state-ia-contractors/adapter.d.ts.map +1 -1
  39. package/out/src/adapters/state-ia-contractors/adapter.js +58 -76
  40. package/out/src/adapters/state-ia-contractors/adapter.js.map +1 -1
  41. package/out/src/adapters/state-ny-notaries/adapter.d.ts.map +1 -1
  42. package/out/src/adapters/state-ny-notaries/adapter.js +67 -85
  43. package/out/src/adapters/state-ny-notaries/adapter.js.map +1 -1
  44. package/out/src/adapters/state-tx-notaries/adapter.d.ts.map +1 -1
  45. package/out/src/adapters/state-tx-notaries/adapter.js +69 -87
  46. package/out/src/adapters/state-tx-notaries/adapter.js.map +1 -1
  47. package/out/src/adapters/synth-po-box/adapter.d.ts.map +1 -1
  48. package/out/src/adapters/synth-po-box/adapter.js +7 -15
  49. package/out/src/adapters/synth-po-box/adapter.js.map +1 -1
  50. package/out/src/adapters/tiger/street-decompose.d.ts.map +1 -1
  51. package/out/src/adapters/tiger/street-decompose.js +18 -27
  52. package/out/src/adapters/tiger/street-decompose.js.map +1 -1
  53. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +3 -3
  54. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts.map +1 -1
  55. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js +62 -80
  56. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js.map +1 -1
  57. package/out/src/adapters/usgov-imls-pls/adapter.d.ts.map +1 -1
  58. package/out/src/adapters/usgov-imls-pls/adapter.js +60 -82
  59. package/out/src/adapters/usgov-imls-pls/adapter.js.map +1 -1
  60. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts.map +1 -1
  61. package/out/src/adapters/usgov-irs-bmf/adapter.js +58 -65
  62. package/out/src/adapters/usgov-irs-bmf/adapter.js.map +1 -1
  63. package/out/src/adapters/usgov-nad/adapter.d.ts.map +1 -1
  64. package/out/src/adapters/usgov-nad/adapter.js +5 -8
  65. package/out/src/adapters/usgov-nad/adapter.js.map +1 -1
  66. package/out/src/adapters/usgov-nppes/adapter.d.ts.map +1 -1
  67. package/out/src/adapters/usgov-nppes/adapter.js +58 -76
  68. package/out/src/adapters/usgov-nppes/adapter.js.map +1 -1
  69. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts.map +1 -1
  70. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js +51 -71
  71. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js.map +1 -1
  72. package/out/src/adapters/wof-admin-jp/adapter.d.ts.map +1 -1
  73. package/out/src/adapters/wof-admin-jp/adapter.js +24 -6
  74. package/out/src/adapters/wof-admin-jp/adapter.js.map +1 -1
  75. package/out/src/build.d.ts.map +1 -1
  76. package/out/src/build.js +2 -1
  77. package/out/src/build.js.map +1 -1
  78. package/out/src/golden.d.ts +5 -0
  79. package/out/src/golden.d.ts.map +1 -1
  80. package/out/src/golden.js +23 -9
  81. package/out/src/golden.js.map +1 -1
  82. package/out/src/parquet-wrapper/reader.d.ts +8 -0
  83. package/out/src/parquet-wrapper/reader.d.ts.map +1 -1
  84. package/out/src/parquet-wrapper/reader.js +16 -0
  85. package/out/src/parquet-wrapper/reader.js.map +1 -1
  86. package/out/src/parquet-wrapper/schema.d.ts +4 -0
  87. package/out/src/parquet-wrapper/schema.d.ts.map +1 -1
  88. package/out/src/parquet-wrapper/schema.js.map +1 -1
  89. package/out/src/parquet.d.ts.map +1 -1
  90. package/out/src/parquet.js +2 -13
  91. package/out/src/parquet.js.map +1 -1
  92. package/out/src/shard-recipes/anchor-absorption.d.ts.map +1 -1
  93. package/out/src/shard-recipes/anchor-absorption.js +2 -1
  94. package/out/src/shard-recipes/anchor-absorption.js.map +1 -1
  95. package/out/src/shard-recipes/country-balanced.d.ts.map +1 -1
  96. package/out/src/shard-recipes/country-balanced.js +68 -60
  97. package/out/src/shard-recipes/country-balanced.js.map +1 -1
  98. package/out/src/shard-recipes/fr-fragment.d.ts.map +1 -1
  99. package/out/src/shard-recipes/fr-fragment.js +8 -5
  100. package/out/src/shard-recipes/fr-fragment.js.map +1 -1
  101. package/out/src/shard-recipes/fr-order.d.ts.map +1 -1
  102. package/out/src/shard-recipes/fr-order.js +14 -58
  103. package/out/src/shard-recipes/fr-order.js.map +1 -1
  104. package/out/src/shard-recipes/german.d.ts.map +1 -1
  105. package/out/src/shard-recipes/german.js +11 -58
  106. package/out/src/shard-recipes/german.js.map +1 -1
  107. package/out/src/shard-recipes/index.d.ts.map +1 -1
  108. package/out/src/shard-recipes/index.js +2 -0
  109. package/out/src/shard-recipes/index.js.map +1 -1
  110. package/out/src/shard-recipes/intersection.d.ts +2 -2
  111. package/out/src/shard-recipes/intersection.d.ts.map +1 -1
  112. package/out/src/shard-recipes/intersection.js +24 -62
  113. package/out/src/shard-recipes/intersection.js.map +1 -1
  114. package/out/src/shard-recipes/locale.d.ts.map +1 -1
  115. package/out/src/shard-recipes/locale.js +14 -16
  116. package/out/src/shard-recipes/locale.js.map +1 -1
  117. package/out/src/shard-recipes/no-fragment.d.ts.map +1 -1
  118. package/out/src/shard-recipes/no-fragment.js +8 -5
  119. package/out/src/shard-recipes/no-fragment.js.map +1 -1
  120. package/out/src/shard-recipes/no-street-led.d.ts.map +1 -1
  121. package/out/src/shard-recipes/no-street-led.js +8 -5
  122. package/out/src/shard-recipes/no-street-led.js.map +1 -1
  123. package/out/src/shard-recipes/po-box-cedex.d.ts +3 -3
  124. package/out/src/shard-recipes/po-box-cedex.d.ts.map +1 -1
  125. package/out/src/shard-recipes/po-box-cedex.js +51 -94
  126. package/out/src/shard-recipes/po-box-cedex.js.map +1 -1
  127. package/out/src/shard-recipes/scaffold.d.ts +59 -9
  128. package/out/src/shard-recipes/scaffold.d.ts.map +1 -1
  129. package/out/src/shard-recipes/scaffold.js +51 -30
  130. package/out/src/shard-recipes/scaffold.js.map +1 -1
  131. package/out/src/shard-recipes/street-affix.d.ts.map +1 -1
  132. package/out/src/shard-recipes/street-affix.js +56 -82
  133. package/out/src/shard-recipes/street-affix.js.map +1 -1
  134. package/out/src/shard-recipes/sub-venue-sources.d.ts +231 -0
  135. package/out/src/shard-recipes/sub-venue-sources.d.ts.map +1 -0
  136. package/out/src/shard-recipes/sub-venue-sources.js +669 -0
  137. package/out/src/shard-recipes/sub-venue-sources.js.map +1 -0
  138. package/out/src/shard-recipes/sub-venue.d.ts +269 -0
  139. package/out/src/shard-recipes/sub-venue.d.ts.map +1 -0
  140. package/out/src/shard-recipes/sub-venue.js +709 -0
  141. package/out/src/shard-recipes/sub-venue.js.map +1 -0
  142. package/out/src/shard-recipes/unit.d.ts +1 -1
  143. package/out/src/shard-recipes/unit.d.ts.map +1 -1
  144. package/out/src/shard-recipes/unit.js +22 -64
  145. package/out/src/shard-recipes/unit.js.map +1 -1
  146. package/out/src/split.d.ts +7 -0
  147. package/out/src/split.d.ts.map +1 -1
  148. package/out/src/split.js +7 -0
  149. package/out/src/split.js.map +1 -1
  150. package/out/src/synthesize.d.ts.map +1 -1
  151. package/out/src/synthesize.js +1 -12
  152. package/out/src/synthesize.js.map +1 -1
  153. package/out/src/tools/align-shard.d.ts.map +1 -1
  154. package/out/src/tools/align-shard.js +3 -7
  155. package/out/src/tools/align-shard.js.map +1 -1
  156. package/out/src/tools/audit.d.ts.map +1 -1
  157. package/out/src/tools/audit.js +4 -4
  158. package/out/src/tools/audit.js.map +1 -1
  159. package/out/src/tools/corpus-stats.d.ts +2 -2
  160. package/out/src/tools/corpus-stats.d.ts.map +1 -1
  161. package/out/src/tools/corpus-stats.js +18 -31
  162. package/out/src/tools/corpus-stats.js.map +1 -1
  163. package/out/src/tools/fetch/download.d.ts.map +1 -1
  164. package/out/src/tools/fetch/download.js +5 -6
  165. package/out/src/tools/fetch/download.js.map +1 -1
  166. package/out/src/tools/fetch/imls-pls.d.ts.map +1 -1
  167. package/out/src/tools/fetch/imls-pls.js +2 -2
  168. package/out/src/tools/fetch/imls-pls.js.map +1 -1
  169. package/out/src/tools/fetch/index.d.ts +14 -1
  170. package/out/src/tools/fetch/index.d.ts.map +1 -1
  171. package/out/src/tools/fetch/index.js +14 -1
  172. package/out/src/tools/fetch/index.js.map +1 -1
  173. package/out/src/tools/fetch/nad.d.ts +1 -1
  174. package/out/src/tools/fetch/nad.js +1 -1
  175. package/out/src/tools/fetch/nppes.d.ts.map +1 -1
  176. package/out/src/tools/fetch/nppes.js +2 -1
  177. package/out/src/tools/fetch/nppes.js.map +1 -1
  178. package/out/src/tools/fetch/openaddresses.d.ts +1 -1
  179. package/out/src/tools/fetch/openaddresses.js +1 -1
  180. package/out/src/tools/fetch/ourairports.d.ts +45 -0
  181. package/out/src/tools/fetch/ourairports.d.ts.map +1 -0
  182. package/out/src/tools/fetch/ourairports.js +123 -0
  183. package/out/src/tools/fetch/ourairports.js.map +1 -0
  184. package/out/src/tools/fetch/wikidata-subvenue.d.ts +171 -0
  185. package/out/src/tools/fetch/wikidata-subvenue.d.ts.map +1 -0
  186. package/out/src/tools/fetch/wikidata-subvenue.js +275 -0
  187. package/out/src/tools/fetch/wikidata-subvenue.js.map +1 -0
  188. package/out/src/tools/golden-expand.d.ts.map +1 -1
  189. package/out/src/tools/golden-expand.js +18 -16
  190. package/out/src/tools/golden-expand.js.map +1 -1
  191. package/out/src/tools/golden-promote.d.ts.map +1 -1
  192. package/out/src/tools/golden-promote.js +12 -6
  193. package/out/src/tools/golden-promote.js.map +1 -1
  194. package/out/src/tools/golden-relabel-street.d.ts +196 -0
  195. package/out/src/tools/golden-relabel-street.d.ts.map +1 -0
  196. package/out/src/tools/golden-relabel-street.js +513 -0
  197. package/out/src/tools/golden-relabel-street.js.map +1 -0
  198. package/out/src/tools/index.d.ts +4 -0
  199. package/out/src/tools/index.d.ts.map +1 -1
  200. package/out/src/tools/index.js +4 -0
  201. package/out/src/tools/index.js.map +1 -1
  202. package/out/src/tools/jsonl-to-parquet.d.ts.map +1 -1
  203. package/out/src/tools/jsonl-to-parquet.js +3 -2
  204. package/out/src/tools/jsonl-to-parquet.js.map +1 -1
  205. package/out/src/tools/lint-shard-vocab.d.ts.map +1 -1
  206. package/out/src/tools/lint-shard-vocab.js +3 -2
  207. package/out/src/tools/lint-shard-vocab.js.map +1 -1
  208. package/out/src/tools/lint-shard.d.ts +3 -3
  209. package/out/src/tools/lint-shard.d.ts.map +1 -1
  210. package/out/src/tools/lint-shard.js +23 -32
  211. package/out/src/tools/lint-shard.js.map +1 -1
  212. package/out/src/tools/overlay-manifest.d.ts.map +1 -1
  213. package/out/src/tools/overlay-manifest.js +2 -1
  214. package/out/src/tools/overlay-manifest.js.map +1 -1
  215. package/out/src/tools/overture-subvenue.d.ts +112 -0
  216. package/out/src/tools/overture-subvenue.d.ts.map +1 -0
  217. package/out/src/tools/overture-subvenue.js +144 -0
  218. package/out/src/tools/overture-subvenue.js.map +1 -0
  219. package/out/src/tools/shard-kryptonite.d.ts +1 -1
  220. package/out/src/tools/shard-kryptonite.d.ts.map +1 -1
  221. package/out/src/tools/shard-kryptonite.js +5 -4
  222. package/out/src/tools/shard-kryptonite.js.map +1 -1
  223. package/out/src/tools/shard-translit.d.ts +6 -3
  224. package/out/src/tools/shard-translit.d.ts.map +1 -1
  225. package/out/src/tools/shard-translit.js +8 -6
  226. package/out/src/tools/shard-translit.js.map +1 -1
  227. package/out/src/tools/sub-venue-lexicon.d.ts +507 -0
  228. package/out/src/tools/sub-venue-lexicon.d.ts.map +1 -0
  229. package/out/src/tools/sub-venue-lexicon.js +817 -0
  230. package/out/src/tools/sub-venue-lexicon.js.map +1 -0
  231. package/out/src/tools/sub-venue-promotions.d.ts +94 -0
  232. package/out/src/tools/sub-venue-promotions.d.ts.map +1 -0
  233. package/out/src/tools/sub-venue-promotions.js +266 -0
  234. package/out/src/tools/sub-venue-promotions.js.map +1 -0
  235. package/out/src/wof-json.d.ts +2 -2
  236. package/out/src/wof-json.d.ts.map +1 -1
  237. package/out/src/wof-json.js +5 -8
  238. package/out/src/wof-json.js.map +1 -1
  239. package/package.json +8 -8
  240. package/src/adapter.ts +58 -1
  241. package/src/adapters/ban/adapter.ts +55 -65
  242. package/src/adapters/ban/street-decompose.ts +17 -25
  243. package/src/adapters/fcc-bdc/adapter.ts +2 -33
  244. package/src/adapters/geonames/adapter.ts +64 -72
  245. package/src/adapters/geonames-postal/adapter.ts +42 -50
  246. package/src/adapters/gnaf/adapter.ts +4 -7
  247. package/src/adapters/gnaf/assemble.ts +6 -10
  248. package/src/adapters/openaddresses/adapter.ts +2 -7
  249. package/src/adapters/overture/adapter.ts +3 -6
  250. package/src/adapters/state-hi-schools/adapter.ts +50 -73
  251. package/src/adapters/state-ia-contractors/adapter.ts +52 -76
  252. package/src/adapters/state-ny-notaries/adapter.ts +61 -85
  253. package/src/adapters/state-tx-notaries/adapter.ts +60 -84
  254. package/src/adapters/synth-po-box/adapter.ts +7 -17
  255. package/src/adapters/tiger/street-decompose.ts +21 -31
  256. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +61 -84
  257. package/src/adapters/usgov-imls-pls/adapter.ts +54 -82
  258. package/src/adapters/usgov-irs-bmf/adapter.ts +53 -63
  259. package/src/adapters/usgov-nad/adapter.ts +5 -8
  260. package/src/adapters/usgov-nppes/adapter.ts +51 -75
  261. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +45 -71
  262. package/src/adapters/wof-admin-jp/adapter.ts +34 -8
  263. package/src/build.ts +2 -1
  264. package/src/golden.ts +24 -9
  265. package/src/parquet-wrapper/reader.ts +19 -0
  266. package/src/parquet-wrapper/schema.ts +4 -0
  267. package/src/parquet.ts +3 -16
  268. package/src/shard-recipes/anchor-absorption.ts +2 -1
  269. package/src/shard-recipes/country-balanced.ts +69 -70
  270. package/src/shard-recipes/fr-fragment.ts +10 -7
  271. package/src/shard-recipes/fr-order.ts +13 -63
  272. package/src/shard-recipes/german.ts +12 -65
  273. package/src/shard-recipes/index.ts +2 -0
  274. package/src/shard-recipes/intersection.ts +32 -60
  275. package/src/shard-recipes/locale.ts +14 -16
  276. package/src/shard-recipes/no-fragment.ts +10 -7
  277. package/src/shard-recipes/no-street-led.ts +10 -7
  278. package/src/shard-recipes/po-box-cedex.ts +68 -119
  279. package/src/shard-recipes/scaffold.ts +82 -28
  280. package/src/shard-recipes/street-affix.ts +58 -95
  281. package/src/shard-recipes/sub-venue-sources.ts +895 -0
  282. package/src/shard-recipes/sub-venue.ts +982 -0
  283. package/src/shard-recipes/unit.ts +22 -70
  284. package/src/split.ts +7 -0
  285. package/src/synthesize.ts +1 -15
  286. package/src/tools/align-shard.ts +3 -6
  287. package/src/tools/audit.ts +6 -5
  288. package/src/tools/corpus-stats.ts +25 -33
  289. package/src/tools/fetch/download.ts +7 -5
  290. package/src/tools/fetch/imls-pls.ts +2 -2
  291. package/src/tools/fetch/index.ts +14 -1
  292. package/src/tools/fetch/nad.ts +1 -1
  293. package/src/tools/fetch/nppes.ts +2 -1
  294. package/src/tools/fetch/openaddresses.ts +1 -1
  295. package/src/tools/fetch/ourairports.ts +166 -0
  296. package/src/tools/fetch/wikidata-subvenue.ts +386 -0
  297. package/src/tools/golden-expand.ts +18 -13
  298. package/src/tools/golden-promote.ts +15 -6
  299. package/src/tools/golden-relabel-street.ts +748 -0
  300. package/src/tools/index.ts +4 -0
  301. package/src/tools/jsonl-to-parquet.ts +3 -2
  302. package/src/tools/lint-shard-vocab.ts +3 -2
  303. package/src/tools/lint-shard.ts +33 -36
  304. package/src/tools/overlay-manifest.ts +2 -1
  305. package/src/tools/overture-subvenue.ts +215 -0
  306. package/src/tools/shard-kryptonite.ts +5 -4
  307. package/src/tools/shard-translit.ts +12 -7
  308. package/src/tools/sub-venue-lexicon.ts +1250 -0
  309. package/src/tools/sub-venue-promotions.ts +330 -0
  310. package/src/wof-json.ts +5 -8
@@ -28,11 +28,9 @@
28
28
  * distribution terms.
29
29
  */
30
30
 
31
- import { createReadStream } from "node:fs"
31
+ import { CSVSpliterator } from "spliterator"
32
32
 
33
- import { parse as csvParse } from "csv-parse"
34
-
35
- import { stableSourceID } from "../../adapter.ts"
33
+ import { splitStreetLine, stableSourceID } from "../../adapter.ts"
36
34
  import { lookupStateAbbreviation } from "../../codex/us-fips-state.ts"
37
35
  import { reconcileComponents } from "../../format.ts"
38
36
  import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
@@ -66,19 +64,6 @@ interface SamhsaSiteRow {
66
64
  frid?: string
67
65
  }
68
66
 
69
- const HOUSE_NUMBER_PREFIX = /^(\d+(?:-\d+)?[A-Za-z]?)\s+(.+)$/
70
-
71
- function splitAddress(address: string): { house_number?: string; street: string } | null {
72
- const trimmed = address.trim()
73
-
74
- if (!trimmed) return null
75
- const m = HOUSE_NUMBER_PREFIX.exec(trimmed)
76
-
77
- if (m) return { house_number: m[1], street: m[2]!.trim() }
78
-
79
- return { street: trimmed }
80
- }
81
-
82
67
  /**
83
68
  * Join the SAMHSA two-line street: primary street + optional secondary line (suite / unit / floor / "behind main
84
69
  * building") on `", "`. The combined value is the `street` component surface form. Phase 1 does not break this out into
@@ -148,76 +133,65 @@ export function createUsgovSamhsaTreatmentLocatorAdapter(): CorpusAdapter {
148
133
  throw new Error(`usgov-samhsa adapter: only US supported, got country=${opts.country}`)
149
134
  }
150
135
 
151
- const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
152
-
153
- const parser = stream.pipe(
154
- csvParse({
155
- columns: true,
156
- skip_empty_lines: true,
157
- relax_quotes: true,
158
- relax_column_count: true,
159
- })
160
- )
136
+ const rows = CSVSpliterator.fromAsync(opts.inputPath, {
137
+ mode: "object",
138
+ normalizeKeys: false,
139
+ enableQuoteHandling: true,
140
+ })
161
141
 
162
142
  let emitted = 0
163
143
 
164
- try {
165
- for await (const record of parser as AsyncIterable<SamhsaSiteRow>) {
166
- if (opts.signal?.aborted) break
167
-
168
- if (opts.limit !== undefined && emitted >= opts.limit) break
144
+ for await (const record of rows as AsyncIterable<SamhsaSiteRow>) {
145
+ if (opts.signal?.aborted) break
169
146
 
170
- const venue = composeVenue(record.name1 ?? "", record.name2)
171
- const street = joinTwoLineStreet(record.street1 ?? "", record.street2)
172
- const split = splitAddress(street)
173
- const city = (record.city ?? "").trim()
174
- const stateAbbr = (record.state ?? "").trim()
175
- const postcode = (record.zip ?? "").trim()
147
+ if (opts.limit !== undefined && emitted >= opts.limit) break
176
148
 
177
- if (!venue || !split || !city || !postcode) continue
178
- const state = lookupStateAbbreviation(stateAbbr)
149
+ const venue = composeVenue(record.name1 ?? "", record.name2)
150
+ const street = joinTwoLineStreet(record.street1 ?? "", record.street2)
151
+ const split = splitStreetLine(street)
152
+ const city = (record.city ?? "").trim()
153
+ const stateAbbr = (record.state ?? "").trim()
154
+ const postcode = (record.zip ?? "").trim()
179
155
 
180
- if (!state) continue
156
+ if (!venue || !split || !city || !postcode) continue
157
+ const state = lookupStateAbbreviation(stateAbbr)
181
158
 
182
- // venue first — same kryptonite-defending insertion order as HRSA.
183
- const components: CanonicalRow["components"] = {
184
- venue,
185
- ...(split.house_number ? { house_number: split.house_number } : {}),
186
- street: split.street,
187
- locality: city,
188
- region: state.abbreviation,
189
- postcode,
190
- }
159
+ if (!state) continue
191
160
 
192
- const raw = composeRaw(venue, split.house_number, split.street, city, state.abbreviation, postcode)
161
+ // venue first — same kryptonite-defending insertion order as HRSA.
162
+ const components: CanonicalRow["components"] = {
163
+ venue,
164
+ ...(split.house_number ? { house_number: split.house_number } : {}),
165
+ street: split.street,
166
+ locality: city,
167
+ region: state.abbreviation,
168
+ postcode,
169
+ }
193
170
 
194
- if (!raw) continue
171
+ const raw = composeRaw(venue, split.house_number, split.street, city, state.abbreviation, postcode)
195
172
 
196
- const aligned = reconcileComponents(components, raw)
173
+ if (!raw) continue
197
174
 
198
- if (!Object.keys(aligned).length) continue
175
+ const aligned = reconcileComponents(components, raw)
199
176
 
200
- const frID = (record.frid ?? "").trim()
177
+ if (!Object.keys(aligned).length) continue
201
178
 
202
- const sourceID = frID
203
- ? `${USGOV_SAMHSA_ADAPTER_ID}-${frID}`
204
- : stableSourceID(USGOV_SAMHSA_ADAPTER_ID, aligned)
179
+ const frID = (record.frid ?? "").trim()
205
180
 
206
- yield {
207
- raw,
208
- components: aligned,
209
- country: "US",
210
- locale: "en-US",
211
- source: USGOV_SAMHSA_ADAPTER_ID,
212
- source_id: sourceID,
213
- corpus_version: "",
214
- license: USGOV_SAMHSA_DEFAULT_LICENSE,
215
- }
181
+ const sourceID = frID ? `${USGOV_SAMHSA_ADAPTER_ID}-${frID}` : stableSourceID(USGOV_SAMHSA_ADAPTER_ID, aligned)
216
182
 
217
- emitted++
183
+ yield {
184
+ raw,
185
+ components: aligned,
186
+ country: "US",
187
+ locale: "en-US",
188
+ source: USGOV_SAMHSA_ADAPTER_ID,
189
+ source_id: sourceID,
190
+ corpus_version: "",
191
+ license: USGOV_SAMHSA_DEFAULT_LICENSE,
218
192
  }
219
- } finally {
220
- stream.destroy()
193
+
194
+ emitted++
221
195
  }
222
196
  },
223
197
  }
@@ -59,15 +59,21 @@ interface NameRow {
59
59
  }
60
60
 
61
61
  /**
62
- * Walk parent chain up to 6 levels.
62
+ * Walk parent chain up to 6 levels, over a place table already resident in memory.
63
+ *
64
+ * `resolve` is the escape hatch for a parent outside the preloaded set (a chain that leaves the country); it is a point
65
+ * query, so keeping the common case out of it is the whole point.
63
66
  */
64
- function chainOf(db: DatabaseSync, startID: number, _jpnNames: Map<number, string>): PlaceRow[] {
65
- const stmt = db.prepare(`SELECT id, name, placetype, parent_id, country FROM spr WHERE id = ?`)
67
+ function chainOf(
68
+ byID: Map<number, PlaceRow>,
69
+ resolve: (id: number) => PlaceRow | undefined,
70
+ startID: number
71
+ ): PlaceRow[] {
66
72
  const out: PlaceRow[] = []
67
73
  let id = startID
68
74
 
69
75
  for (let i = 0; i < MAX_ANCESTRY_DEPTH && id > 0; i++) {
70
- const row = stmt.get(id) as PlaceRow | undefined
76
+ const row = byID.get(id) ?? resolve(id)
71
77
 
72
78
  if (!row) break
73
79
  out.push(row)
@@ -151,9 +157,29 @@ export function createWOFAdminJpAdapter(): CorpusAdapter {
151
157
  }
152
158
  }
153
159
 
154
- const seeds = db.prepare(`SELECT id FROM spr WHERE country='JP' AND placetype='neighbourhood'`).all() as {
155
- id: number
156
- }[]
160
+ // One read of the JP place table instead of a fresh `prepare` + up to six point queries per
161
+ // seed; there are tens of thousands of neighbourhood seeds.
162
+ const byID = new Map<number, PlaceRow>()
163
+
164
+ for (const row of db
165
+ .prepare(`SELECT id, name, placetype, parent_id, country FROM spr WHERE country='JP'`)
166
+ .all() as unknown as PlaceRow[]) {
167
+ byID.set(row.id, row)
168
+ }
169
+
170
+ const outsideStmt = db.prepare(`SELECT id, name, placetype, parent_id, country FROM spr WHERE id = ?`)
171
+
172
+ const resolveOutside = (id: number): PlaceRow | undefined => {
173
+ const row = outsideStmt.get(id) as unknown as PlaceRow | undefined
174
+
175
+ if (row) {
176
+ byID.set(id, row)
177
+ }
178
+
179
+ return row
180
+ }
181
+
182
+ const seeds = [...byID.values()].filter((row) => row.placetype === "neighbourhood")
157
183
 
158
184
  let emitted = 0
159
185
 
@@ -162,7 +188,7 @@ export function createWOFAdminJpAdapter(): CorpusAdapter {
162
188
 
163
189
  if (opts.limit !== undefined && emitted >= opts.limit) break
164
190
 
165
- const chain = chainOf(db, seed.id, jpnNames)
191
+ const chain = chainOf(byID, resolveOutside, seed.id)
166
192
  const synth = synthesizeJpAddress(chain, jpnNames)
167
193
 
168
194
  if (!synth) continue
package/src/build.ts CHANGED
@@ -52,6 +52,7 @@ import { mkdir, writeFile } from "node:fs/promises"
52
52
  import { join } from "node:path"
53
53
 
54
54
  import { $public } from "@mailwoman/core/env"
55
+ import { parseJSONStrict } from "@mailwoman/core/objects"
55
56
  import { JSONSpliterator } from "spliterator"
56
57
 
57
58
  import { defaultAdapterRegistry } from "./adapter.ts"
@@ -188,7 +189,7 @@ export async function buildCorpus(opts: BuildCorpusOptions): Promise<BuildCorpus
188
189
  existsSync(cachedManifest) &&
189
190
  existsSync(join(adapterDir, "canonical.jsonl"))
190
191
  ) {
191
- const cached = JSON.parse(readFileSync(cachedManifest, "utf8")) as AdapterRunManifest
192
+ const cached = parseJSONStrict<AdapterRunManifest>(readFileSync(cachedManifest, "utf8"))
192
193
  opts.onProgress?.("adapter-run", `resumed ${adapter.id} (reused ${cached.yielded} canonical rows)`)
193
194
  adapterRuns.push(cached)
194
195
 
package/src/golden.ts CHANGED
@@ -19,10 +19,11 @@
19
19
  * creep in over time as new entries land.
20
20
  */
21
21
 
22
- import { readdir, readFile } from "node:fs/promises"
23
- import { extname, join } from "node:path"
22
+ import { readdir } from "node:fs/promises"
24
23
 
25
24
  import { COMPONENT_TAGS, type ComponentTag } from "@mailwoman/core/types"
25
+ import { join } from "path-ts"
26
+ import { TextSpliterator } from "spliterator"
26
27
 
27
28
  import { reconcileComponents } from "./format.ts"
28
29
 
@@ -61,6 +62,9 @@ export interface GoldenReport {
61
62
  * Parse a single JSONL line into a `GoldenEntry`. Throws on schema violations.
62
63
  */
63
64
  export function parseGoldenLine(line: string): GoldenEntry {
65
+ // The throw IS the result: `validateGoldenFile` catches it and records the message against the line
66
+ // number, so a tolerant parse would report a corrupt row as valid.
67
+ // oxlint-disable-next-line no-restricted-properties
64
68
  const obj = JSON.parse(line) as Partial<GoldenEntry> & Record<string, unknown>
65
69
 
66
70
  if (typeof obj.raw !== "string" || !obj.raw.length) {
@@ -112,16 +116,23 @@ export function unreachableComponents(entry: GoldenEntry): ComponentTag[] {
112
116
 
113
117
  /**
114
118
  * Validate one `.jsonl` file end-to-end, returning a list of issues.
119
+ *
120
+ * Parses line by line over `TextSpliterator` rather than `JSONSpliterator`: every issue this returns carries the LINE
121
+ * NUMBER it was found on, and a malformed line has to be REPORTED rather than thrown. `JSONSpliterator` parses each row
122
+ * for you and throws on the first bad one — correct for consumers that want the rows, wrong for the validator whose
123
+ * whole job is locating the bad ones.
115
124
  */
116
125
  export async function validateGoldenFile(path: string): Promise<GoldenIssue[]> {
117
- const text = await readFile(path, "utf8")
118
- const lines = text.split("\n")
119
126
  const issues: GoldenIssue[] = []
127
+ // Counted over EVERY row including blanks, so the number matches what an editor shows.
128
+ let lineNumber = 0
120
129
 
121
- for (let i = 0; i < lines.length; i++) {
122
- const line = lines[i]!.trim()
130
+ for await (const raw of TextSpliterator.fromAsync(path, { skipEmpty: false })) {
131
+ lineNumber++
132
+ const line = raw.trim()
123
133
 
124
134
  if (!line) continue
135
+ const i = lineNumber - 1
125
136
 
126
137
  try {
127
138
  const entry = parseGoldenLine(line)
@@ -146,7 +157,7 @@ export async function validateGoldenFile(path: string): Promise<GoldenIssue[]> {
146
157
  * Validate every `.jsonl` in a golden directory.
147
158
  */
148
159
  export async function validateGoldenDir(dir: string): Promise<GoldenReport> {
149
- const files = (await readdir(dir)).filter((n) => extname(n) === ".jsonl").toSorted()
160
+ const files = (await readdir(dir)).filter((n) => n.endsWith(".jsonl")).toSorted()
150
161
  const issues: GoldenIssue[] = []
151
162
  let entries = 0
152
163
 
@@ -154,8 +165,12 @@ export async function validateGoldenDir(dir: string): Promise<GoldenReport> {
154
165
  const fullPath = join(dir, name)
155
166
  const fileIssues = await validateGoldenFile(fullPath)
156
167
  issues.push(...fileIssues)
157
- const text = await readFile(fullPath, "utf8")
158
- entries += text.split("\n").filter((l) => l.trim()).length
168
+
169
+ for await (const line of TextSpliterator.fromAsync(fullPath)) {
170
+ if (line.trim()) {
171
+ entries++
172
+ }
173
+ }
159
174
  }
160
175
 
161
176
  return { entries, files: files.length, issues }
@@ -59,6 +59,25 @@ export class ParquetReader<T extends ParquetRecordLike> extends BaseParquetReade
59
59
  return super[Symbol.asyncIterator]() as AsyncGenerator<T, void, unknown>
60
60
  }
61
61
 
62
+ /**
63
+ * Iterate a subset of the columns, narrowed to the keys asked for.
64
+ *
65
+ * Parquet is columnar, so a projection is read avoided rather than read-then-discarded: on a 14-column corpus shard,
66
+ * two columns cost 93 ms against 253 ms for the whole row. The default iterator reads every column, which is what a
67
+ * caller wanting the whole record should use.
68
+ */
69
+ public async *project<K extends keyof T>(...columns: K[]): AsyncGenerator<Pick<T, K>, void, unknown> {
70
+ const cursor = this.getCursor(columns.map((column) => [column as string]))
71
+
72
+ for (;;) {
73
+ const row = (await cursor.next()) as Pick<T, K> | null
74
+
75
+ if (!row) return
76
+
77
+ yield row
78
+ }
79
+ }
80
+
62
81
  public async [Symbol.asyncDispose]() {
63
82
  return this.close()
64
83
  }
@@ -24,6 +24,10 @@ import { LRUCache } from "lru-cache"
24
24
  /**
25
25
  * A Parquet record-like object, i.e. a record with string keys and JSON-serializable values.
26
26
  */
27
+ /**
28
+ * Shape a row type must satisfy. Declare yours as a `type` alias — TypeScript gives an alias an implicit index
29
+ * signature and an `interface` none, so an otherwise-identical interface does not satisfy this.
30
+ */
27
31
  export type ParquetRecordLike = Record<string, unknown | undefined>
28
32
 
29
33
  /**
package/src/parquet.ts CHANGED
@@ -37,11 +37,12 @@
37
37
  * close — cheap relative to writing it).
38
38
  */
39
39
 
40
- import { createHash } from "node:crypto"
41
40
  import { createReadStream } from "node:fs"
42
41
  import { mkdir, stat, writeFile } from "node:fs/promises"
43
42
  import { join } from "node:path"
44
43
 
44
+ import { sha256File } from "@mailwoman/core/utils"
45
+
45
46
  import { ParquetWriter, type ParquetSchemaDefinition } from "./parquet-wrapper/index.ts"
46
47
  import type { SplitName } from "./split.ts"
47
48
  import type { LabeledRow } from "./types.ts"
@@ -311,7 +312,7 @@ export async function writeShards(perSplit: PerSplitRows, opts: WriteShardsOptio
311
312
 
312
313
  if (shardRows > 0) {
313
314
  const fileStat = await stat(path)
314
- const sha256 = await hashFile(path)
315
+ const sha256 = await sha256File(path)
315
316
 
316
317
  shards.push({
317
318
  split,
@@ -375,17 +376,3 @@ export async function writeShards(perSplit: PerSplitRows, opts: WriteShardsOptio
375
376
 
376
377
  return manifest
377
378
  }
378
-
379
- /**
380
- * Single-pass SHA-256 over the file at `path`. Cheap relative to Parquet write throughput.
381
- */
382
- async function hashFile(path: string): Promise<string> {
383
- const hash = createHash("sha256")
384
- const stream = createReadStream(path)
385
-
386
- for await (const chunk of stream) {
387
- hash.update(chunk as Buffer)
388
- }
389
-
390
- return hash.digest("hex")
391
- }
@@ -14,6 +14,7 @@
14
14
 
15
15
  import { readFileSync } from "node:fs"
16
16
 
17
+ import { parseJSONStrict } from "@mailwoman/core/objects"
17
18
  import { dataRootPath } from "@mailwoman/core/utils"
18
19
 
19
20
  import { stableSourceID } from "../adapter.ts"
@@ -31,7 +32,7 @@ const ANCHOR_LOOKUP = dataRootPath("anchor", "pilot-anchor-lookup.json")
31
32
  * The real US ZIPs in the anchor lookup (entries whose value is a `[{ US: … }]` candidate list).
32
33
  */
33
34
  function loadRealUsZips(path: string): string[] {
34
- const d = JSON.parse(readFileSync(path, "utf8")) as Record<string, unknown>
35
+ const d = parseJSONStrict<Record<string, unknown>>(readFileSync(path, "utf8"))
35
36
  const zips: string[] = []
36
37
 
37
38
  for (const [pc, v] of Object.entries(d)) {
@@ -28,11 +28,12 @@ import { spawnSync } from "node:child_process"
28
28
 
29
29
  import { COUNTRY_SURFACE_FORMS, CountryNames } from "@mailwoman/codex/country"
30
30
  import type { ComponentTag } from "@mailwoman/core/types"
31
+ import { dataRootPath } from "@mailwoman/core/utils"
31
32
 
32
33
  import { stableSourceID } from "../adapter.ts"
33
34
  import { alignRow } from "../align.ts"
34
35
  import type { CanonicalRow } from "../types.ts"
35
- import { makeMulberry32, type ShardRecipe } from "./scaffold.ts"
36
+ import { makeMulberry32, readCSVRecords, type ShardRecipe } from "./scaffold.ts"
36
37
 
37
38
  // v2: the country TOKEN is decoupled from the skeleton's locale and drawn from a BROAD pool — every
38
39
  // ISO canonical name + every curated surface form (endonyms/abbrevs). Surface forms are over-weighted
@@ -72,24 +73,72 @@ interface CountrySource {
72
73
  * countrywide extracts (FR/IT/NL) read region from the CSV when present.
73
74
  */
74
75
  const SOURCES: readonly CountrySource[] = [
75
- { zip: "/tmp/oa-cache/us__ia__statewide.zip", csv: "us/ia/statewide.csv", iso2: "US", region: "IA", order: "us" },
76
- { zip: "/tmp/oa-cache/us__il__cook.zip", csv: "us/il/cook.csv", iso2: "US", region: "IL", order: "us" },
77
- { zip: "/tmp/oa-cache/us__mt__statewide.zip", csv: "us/mt/statewide.csv", iso2: "US", region: "MT", order: "us" },
78
- { zip: "/tmp/oa-cache/us__sd__statewide.zip", csv: "us/sd/statewide.csv", iso2: "US", region: "SD", order: "us" },
79
- { zip: "/tmp/oa-cache/de__sn__statewide.zip", csv: "de/sn/statewide.csv", iso2: "DE", region: "", order: "eu" },
80
- { zip: "/tmp/oa-cache/fr__countrywide.zip", csv: "fr/countrywide.csv", iso2: "FR", region: "", order: "fr" },
76
+ {
77
+ zip: dataRootPath("oa-cache", "us__ia__statewide.zip"),
78
+ csv: "us/ia/statewide.csv",
79
+ iso2: "US",
80
+ region: "IA",
81
+ order: "us",
82
+ },
83
+ { zip: dataRootPath("oa-cache", "us__il__cook.zip"), csv: "us/il/cook.csv", iso2: "US", region: "IL", order: "us" },
84
+ {
85
+ zip: dataRootPath("oa-cache", "us__mt__statewide.zip"),
86
+ csv: "us/mt/statewide.csv",
87
+ iso2: "US",
88
+ region: "MT",
89
+ order: "us",
90
+ },
91
+ {
92
+ zip: dataRootPath("oa-cache", "us__sd__statewide.zip"),
93
+ csv: "us/sd/statewide.csv",
94
+ iso2: "US",
95
+ region: "SD",
96
+ order: "us",
97
+ },
98
+ {
99
+ zip: dataRootPath("oa-cache", "de__sn__statewide.zip"),
100
+ csv: "de/sn/statewide.csv",
101
+ iso2: "DE",
102
+ region: "",
103
+ order: "eu",
104
+ },
105
+ {
106
+ zip: dataRootPath("oa-cache", "fr__countrywide.zip"),
107
+ csv: "fr/countrywide.csv",
108
+ iso2: "FR",
109
+ region: "",
110
+ order: "fr",
111
+ },
81
112
  // ES uses the Spanish IGN schema, not the OA standard columns — skipped here (codex still recognizes
82
113
  // "España"/"Spain"). A dedicated IGN adapter is a follow-up.
83
- { zip: "/tmp/oa-cache/it__countrywide.zip", csv: "it/countrywide.csv", iso2: "IT", region: "", order: "eu" },
84
- { zip: "/tmp/oa-cache/nl__countrywide.zip", csv: "nl/countrywide.csv", iso2: "NL", region: "", order: "eu" },
114
+ {
115
+ zip: dataRootPath("oa-cache", "it__countrywide.zip"),
116
+ csv: "it/countrywide.csv",
117
+ iso2: "IT",
118
+ region: "",
119
+ order: "eu",
120
+ },
121
+ {
122
+ zip: dataRootPath("oa-cache", "nl__countrywide.zip"),
123
+ csv: "nl/countrywide.csv",
124
+ iso2: "NL",
125
+ region: "",
126
+ order: "eu",
127
+ },
85
128
  ]
86
129
 
87
130
  /**
88
131
  * Held-out for --golden: Vermont (US holdout) + Berlin (DE holdout) — geographic split, never trained.
89
132
  */
90
133
  const EVAL_SOURCES: readonly CountrySource[] = [
91
- { zip: "/tmp/oa-cache/us__vt__statewide.zip", csv: "us/vt/statewide.csv", iso2: "US", region: "VT", order: "us" },
92
- { zip: "/tmp/oa-cache/de__berlin.zip", csv: "de/berlin.csv", iso2: "DE", region: "", order: "eu" },
134
+ {
135
+ zip: dataRootPath("oa-cache", "us__vt__statewide.zip"),
136
+ csv: "us/vt/statewide.csv",
137
+ iso2: "US",
138
+ region: "VT",
139
+ order: "us",
140
+ },
141
+ { zip: dataRootPath("oa-cache", "de__berlin.zip"), csv: "de/berlin.csv", iso2: "DE", region: "", order: "eu" },
93
142
  ]
94
143
 
95
144
  /**
@@ -105,45 +154,9 @@ interface CountryTuple {
105
154
  order: string
106
155
  }
107
156
 
108
- function splitCSV(line: string): string[] {
109
- const out: string[] = []
110
-
111
- let cur = "",
112
- inQ = false
113
-
114
- for (let i = 0; i < line.length; i++) {
115
- const c = line[i]
116
-
117
- if (inQ) {
118
- if (c === '"') {
119
- if (line[i + 1] === '"') {
120
- cur += '"'
121
-
122
- i++
123
- } else {
124
- inQ = false
125
- }
126
- } else {
127
- cur += c
128
- }
129
- } else if (c === '"') {
130
- inQ = true
131
- } else if (c === ",") {
132
- out.push(cur)
133
- cur = ""
134
- } else {
135
- cur += c
136
- }
137
- }
138
-
139
- out.push(cur)
140
-
141
- return out
142
- }
143
-
144
157
  function readTuples(source: CountrySource, limit: number): CountryTuple[] {
145
158
  // countrywide extracts (FR/IT/NL) are GB-scale — cap the bytes with `head` (read ~8 lines per wanted
146
- // tuple to survive dedup/skips) so the toString stays under V8's string limit.
159
+ // tuple to survive dedup/skips) so the whole extract never has to be held in memory.
147
160
  const maxLines = Math.max(limit * 8, 20_000) + 1
148
161
 
149
162
  const r = spawnSync("bash", ["-c", `unzip -p "${source.zip}" "${source.csv}" | head -n ${maxLines}`], {
@@ -157,29 +170,15 @@ function readTuples(source: CountrySource, limit: number): CountryTuple[] {
157
170
  return []
158
171
  }
159
172
 
160
- const lines = r.stdout.toString("utf8").split(/\r?\n/)
161
-
162
- if (lines.length < 2) return []
163
- const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
164
- const idx = (n: string): number => header.indexOf(n)
165
-
166
- const iNum = idx("number"),
167
- iStreet = idx("street"),
168
- iCity = idx("city"),
169
- iRegion = idx("region"),
170
- iPost = idx("postcode")
171
-
172
- const get = (cells: string[], i: number): string => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "")
173
173
  const tuples: CountryTuple[] = []
174
174
  const seen = new Set<string>()
175
175
 
176
- for (let li = 1; li < lines.length && tuples.length < limit; li++) {
177
- if (!lines[li]) continue
178
- const cells = splitCSV(lines[li]!)
176
+ for (const row of readCSVRecords(r.stdout)) {
177
+ if (tuples.length >= limit) break
179
178
 
180
- const street = get(cells, iStreet),
181
- locality = get(cells, iCity),
182
- house_number = get(cells, iNum)
179
+ const street = row.street ?? "",
180
+ locality = row.city ?? "",
181
+ house_number = row.number ?? ""
183
182
 
184
183
  if (!street || !locality || !house_number) continue
185
184
  const key = `${house_number}|${street}|${locality}`.toLowerCase()
@@ -191,8 +190,8 @@ function readTuples(source: CountrySource, limit: number): CountryTuple[] {
191
190
  house_number,
192
191
  street,
193
192
  locality,
194
- region: get(cells, iRegion) || source.region,
195
- postcode: get(cells, iPost),
193
+ region: row.region || source.region,
194
+ postcode: row.postcode ?? "",
196
195
  iso2: source.iso2,
197
196
  order: source.order,
198
197
  })
@@ -460,7 +459,7 @@ export const countryBalancedRecipe: ShardRecipe = {
460
459
  }
461
460
 
462
461
  if (!pool.length) {
463
- throw new Error("No tuples — are the cached OA zips present in /tmp/oa-cache?")
462
+ throw new Error(`No tuples — are the cached OA zips present in ${dataRootPath("oa-cache")}?`)
464
463
  }
465
464
 
466
465
  let emitted = 0
@@ -68,7 +68,7 @@
68
68
  * blow-up). Disable that mask for any run including this shard.
69
69
  */
70
70
 
71
- import { readFileSync } from "node:fs"
71
+ import { TextSpliterator } from "spliterator"
72
72
 
73
73
  import { decomposeFrStreet } from "../adapters/ban/street-decompose.ts"
74
74
  import { alignAndWrite, makeMulberry32, readTuples, type ShardRecipe, shardSourceID } from "./scaffold.ts"
@@ -187,12 +187,15 @@ export const frFragmentRecipe: ShardRecipe = {
187
187
  )
188
188
  }
189
189
 
190
- const excluded = new Set(
191
- readFileSync(excludePath, "utf8")
192
- .split("\n")
193
- .map((line) => line.trim())
194
- .filter((line) => line && !line.startsWith("#"))
195
- )
190
+ const excluded = new Set<string>()
191
+
192
+ for await (const line of TextSpliterator.fromAsync(excludePath)) {
193
+ const trimmed = line.trim()
194
+
195
+ if (trimmed && !trimmed.startsWith("#")) {
196
+ excluded.add(trimmed)
197
+ }
198
+ }
196
199
 
197
200
  if (!excluded.size) throw new Error(`fr-fragment: --exclude-surfaces "${excludePath}" listed no surfaces`)
198
201