@mailwoman/corpus 8.6.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
@@ -25,14 +25,13 @@
25
25
  * conservative `ODbL-1.0` label, which wrongly read as Tier-C-denied in the corpus license audit.
26
26
  * The model card MUST carry the BAN attribution (Tier B obligation).
27
27
  *
28
- * The adapter is streaming-aware: it uses `csv-parse` in streaming mode so a 25M-row dump never
29
- * sits in memory. Honors `opts.limit` for fixture / smoke runs, `opts.signal` for cancellation,
30
- * and `opts.country` for a self-consistency check (errors if country !== FR).
28
+ * The adapter is streaming-aware: `CSVSpliterator.fromAsync` reads the `;`-delimited dump row by
29
+ * row, so a 25M-row file never sits in memory. Honors `opts.limit` for fixture / smoke runs,
30
+ * `opts.signal` for cancellation, and `opts.country` for a self-consistency check (errors if
31
+ * country !== FR).
31
32
  */
32
33
 
33
- import { createReadStream } from "node:fs"
34
-
35
- import { parse as csvParse } from "csv-parse"
34
+ import { CSVSpliterator } from "spliterator"
36
35
 
37
36
  import { stableSourceID } from "../../adapter.ts"
38
37
  import { reconcileComponents } from "../../format.ts"
@@ -108,86 +107,77 @@ export function createBanAdapter(): CorpusAdapter {
108
107
  throw new Error(`ban adapter: only FR supported, got country=${opts.country}`)
109
108
  }
110
109
 
111
- const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
112
-
113
- const parser = stream.pipe(
114
- csvParse({
115
- delimiter: ";",
116
- columns: true,
117
- skip_empty_lines: true,
118
- relax_quotes: true,
119
- relax_column_count: true,
120
- })
121
- )
110
+ const rows = CSVSpliterator.fromAsync(opts.inputPath, {
111
+ mode: "object",
112
+ normalizeKeys: false,
113
+ columnDelimiter: ";",
114
+ enableQuoteHandling: true,
115
+ })
122
116
 
123
117
  let emitted = 0
124
118
 
125
- try {
126
- for await (const record of parser as AsyncIterable<BanRow>) {
127
- if (opts.signal?.aborted) break
128
-
129
- if (opts.limit !== undefined && emitted >= opts.limit) break
119
+ for await (const record of rows as AsyncIterable<BanRow>) {
120
+ if (opts.signal?.aborted) break
130
121
 
131
- const house = composeHouseNumber(record.numero ?? "", record.rep ?? "")
132
- const street = (record.nom_voie ?? "").trim()
133
- const postcode = (record.code_postal ?? "").trim()
134
- const locality = (record.nom_commune ?? "").trim()
122
+ if (opts.limit !== undefined && emitted >= opts.limit) break
135
123
 
136
- if (!street || !locality) continue
124
+ const house = composeHouseNumber(record.numero ?? "", record.rep ?? "")
125
+ const street = (record.nom_voie ?? "").trim()
126
+ const postcode = (record.code_postal ?? "").trim()
127
+ const locality = (record.nom_commune ?? "").trim()
137
128
 
138
- if (!house && !postcode) continue
129
+ if (!street || !locality) continue
139
130
 
140
- const decomposed = decomposeFrStreet(street)
131
+ if (!house && !postcode) continue
141
132
 
142
- const components: CanonicalRow["components"] = {}
133
+ const decomposed = decomposeFrStreet(street)
143
134
 
144
- if (house) {
145
- components.house_number = house
146
- }
135
+ const components: CanonicalRow["components"] = {}
147
136
 
148
- if (decomposed.prefix) {
149
- components.street_prefix = decomposed.prefix
150
- }
137
+ if (house) {
138
+ components.house_number = house
139
+ }
151
140
 
152
- if (decomposed.street) {
153
- components.street = decomposed.street
154
- }
141
+ if (decomposed.prefix) {
142
+ components.street_prefix = decomposed.prefix
143
+ }
155
144
 
156
- if (postcode) {
157
- components.postcode = postcode
158
- }
145
+ if (decomposed.street) {
146
+ components.street = decomposed.street
147
+ }
159
148
 
160
- if (locality) {
161
- components.locality = locality
162
- }
149
+ if (postcode) {
150
+ components.postcode = postcode
151
+ }
163
152
 
164
- const raw = composeRaw(house, street, postcode, locality)
153
+ if (locality) {
154
+ components.locality = locality
155
+ }
165
156
 
166
- if (!raw) continue
157
+ const raw = composeRaw(house, street, postcode, locality)
167
158
 
168
- const aligned = reconcileComponents(components, raw)
159
+ if (!raw) continue
169
160
 
170
- if (!Object.keys(aligned).length) continue
161
+ const aligned = reconcileComponents(components, raw)
171
162
 
172
- const sourceID = record.id?.trim()
173
- ? `${BAN_ADAPTER_ID}-${record.id.trim()}`
174
- : stableSourceID(BAN_ADAPTER_ID, aligned)
163
+ if (!Object.keys(aligned).length) continue
175
164
 
176
- yield {
177
- raw,
178
- components: aligned,
179
- country: "FR",
180
- locale: "fr-FR",
181
- source: BAN_ADAPTER_ID,
182
- source_id: sourceID,
183
- corpus_version: "",
184
- license: "Licence Ouverte 2.0",
185
- }
165
+ const sourceID = record.id?.trim()
166
+ ? `${BAN_ADAPTER_ID}-${record.id.trim()}`
167
+ : stableSourceID(BAN_ADAPTER_ID, aligned)
186
168
 
187
- emitted++
169
+ yield {
170
+ raw,
171
+ components: aligned,
172
+ country: "FR",
173
+ locale: "fr-FR",
174
+ source: BAN_ADAPTER_ID,
175
+ source_id: sourceID,
176
+ corpus_version: "",
177
+ license: "Licence Ouverte 2.0",
188
178
  }
189
- } finally {
190
- stream.destroy()
179
+
180
+ emitted++
191
181
  }
192
182
  },
193
183
  }
@@ -16,9 +16,9 @@
16
16
  */
17
17
 
18
18
  import { readFileSync } from "node:fs"
19
- import { resolve } from "node:path"
20
19
 
21
20
  import { resourceDictionaryPath } from "@mailwoman/core/utils"
21
+ import { TextSpliterator } from "spliterator"
22
22
 
23
23
  /**
24
24
  * Tokens a BAN street needs before a type/article/name decomposition is attempted.
@@ -26,38 +26,30 @@ import { resourceDictionaryPath } from "@mailwoman/core/utils"
26
26
  const MIN_TOKENS_FOR_DECOMPOSE = 3
27
27
 
28
28
  function loadDictionary(filename: string): Set<string> {
29
- const candidates = [
30
- resourceDictionaryPath("libpostal", "fr", filename),
31
- resourceDictionaryPath("libpostal", "fr", filename),
32
- resolve(process.cwd(), "core/data/libpostal/dictionaries/fr", filename),
33
- ]
29
+ // `resourceDictionaryPath` already resolves both layouts — `core/data/...` from source and from the
30
+ // packaged `out/` tree. The candidate list this replaced named it TWICE and then guessed a third path
31
+ // off `process.cwd()`, and swallowed every error while probing, so a corrupt dictionary reported as a
32
+ // missing one.
33
+ const text = readFileSync(resourceDictionaryPath("libpostal", "fr", filename), "utf8")
34
+ const set = new Set<string>()
34
35
 
35
- for (const path of candidates) {
36
- try {
37
- const text = readFileSync(path, "utf8")
38
- const set = new Set<string>()
36
+ // The largest libpostal dictionary is 8.4 KB, and this runs once per process at module load.
37
+ for (const line of TextSpliterator.from(text)) {
38
+ const trimmed = line.trim()
39
39
 
40
- for (const line of text.split("\n")) {
41
- const trimmed = line.trim()
40
+ if (!trimmed || trimmed.startsWith("#")) continue
42
41
 
43
- if (!trimmed || trimmed.startsWith("#")) continue
42
+ // libpostal format: canonical|abbr|abbr|... — index all forms
43
+ for (const form of trimmed.split("|")) {
44
+ const f = form.trim().toLowerCase()
44
45
 
45
- for (const form of trimmed.split("|")) {
46
- const f = form.trim().toLowerCase()
47
-
48
- if (f) {
49
- set.add(f)
50
- }
51
- }
46
+ if (f) {
47
+ set.add(f)
52
48
  }
53
-
54
- return set
55
- } catch {
56
- // try next
57
49
  }
58
50
  }
59
51
 
60
- throw new Error(`Could not load FR libpostal dictionary: ${filename}`)
52
+ return set
61
53
  }
62
54
 
63
55
  const STREET_TYPES_FR = loadDictionary("street_types.txt")
@@ -36,6 +36,7 @@
36
36
 
37
37
  import { DatabaseSync } from "node:sqlite"
38
38
 
39
+ import { splitStreetLine } from "../../adapter.ts"
39
40
  import { lookupStateAbbreviation } from "../../codex/us-fips-state.ts"
40
41
  import { formatAddress, reconcileComponents } from "../../format.ts"
41
42
  import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
@@ -63,38 +64,6 @@ interface BdcLocationRow {
63
64
  zip_suffix: string | null
64
65
  }
65
66
 
66
- /**
67
- * Split `address_primary` into a `(house_number, street)` pair.
68
- *
69
- * BDC's `address_primary` follows USPS Publication 28 conventions but with hand-entry drift. The canonical
70
- * leading-digit prefix is the house number (`"123 Main St"`, `"6450 W Indian School Rd"`, even hyphenated forms `"40-12
71
- * Bell Blvd"`). Anything that doesn't match the prefix shape (`"PO Box 1234"`, `"RR 2 Box 67"`, `"HC 1"`) is left as a
72
- * single `street` value — the model sees the original surface form, and downstream classifiers/po-box handling can pick
73
- * it up.
74
- *
75
- * The regex tolerates one trailing letter (`"123A Main St"`) and an optional hyphenated half (`"40-12"`) which is
76
- * common in NYC + suburban garden-apartment numbering.
77
- */
78
- const HOUSE_NUMBER_PREFIX = /^(\d+(?:-\d+)?[A-Za-z]?)\s+(.+)$/
79
-
80
- interface SplitAddress {
81
- house_number?: string
82
- street: string
83
- }
84
-
85
- export function splitAddressPrimary(address: string): SplitAddress | null {
86
- const trimmed = address.trim()
87
-
88
- if (!trimmed) return null
89
- const match = HOUSE_NUMBER_PREFIX.exec(trimmed)
90
-
91
- if (match) {
92
- return { house_number: match[1], street: match[2]!.trim() }
93
- }
94
-
95
- return { street: trimmed }
96
- }
97
-
98
67
  /**
99
68
  * Combine `zip` + optional `zip_suffix` into the canonical USPS postcode surface form.
100
69
  *
@@ -150,7 +119,7 @@ export function createFccBdcAdapter(): CorpusAdapter {
150
119
 
151
120
  if (opts.limit !== undefined && emitted >= opts.limit) return
152
121
 
153
- const split = splitAddressPrimary(row.address_primary ?? "")
122
+ const split = splitStreetLine(row.address_primary ?? "")
154
123
 
155
124
  if (!split) continue
156
125
  const state = lookupStateAbbreviation(row.state)
@@ -28,10 +28,10 @@
28
28
  * License: stamped `"CC-BY-4.0"` per row (GeoNames' terms); provenance is the `geonames-<id>` key.
29
29
  */
30
30
 
31
- import { createReadStream, existsSync, readFileSync } from "node:fs"
32
- import { dirname, join } from "node:path"
31
+ import { existsSync } from "node:fs"
33
32
 
34
- import { parse as csvParse } from "csv-parse"
33
+ import { dirname, join } from "path-ts"
34
+ import { TSVSpliterator } from "spliterator"
35
35
 
36
36
  import { stableSourceID } from "../../adapter.ts"
37
37
  import { reconcileComponents } from "../../format.ts"
@@ -69,16 +69,14 @@ const NON_CURRENT_PPL = new Set(["PPLH", "PPLQ", "PPLW", "PPLCH"])
69
69
  /**
70
70
  * Load `admin1CodesASCII.txt` → Map("<CC>.<admin1>" → region name). Empty map if absent.
71
71
  */
72
- function loadAdmin1(dir: string): Map<string, string> {
72
+ async function loadAdmin1(dir: string): Promise<Map<string, string>> {
73
73
  const map = new Map<string, string>()
74
74
  const fp = join(dir, "admin1CodesASCII.txt")
75
75
 
76
76
  if (!existsSync(fp)) return map
77
77
 
78
- for (const line of readFileSync(fp, "utf8").split("\n")) {
79
- if (!line) continue
80
- const cols = line.split("\t")
81
-
78
+ // `header: false` — the file is headerless, and the spliterator eats row 1 as a header otherwise.
79
+ for await (const cols of TSVSpliterator.fromAsync(fp, { header: false })) {
82
80
  if (cols[0] && cols[1]) {
83
81
  map.set(cols[0], cols[1])
84
82
  }
@@ -90,15 +88,16 @@ function loadAdmin1(dir: string): Map<string, string> {
90
88
  /**
91
89
  * Load `countryInfo.txt` → Map(ISO → country name). Empty map if absent. The file is `#`-commented.
92
90
  */
93
- function loadCountries(dir: string): Map<string, string> {
91
+ async function loadCountries(dir: string): Promise<Map<string, string>> {
94
92
  const map = new Map<string, string>()
95
93
  const fp = join(dir, "countryInfo.txt")
96
94
 
97
95
  if (!existsSync(fp)) return map
98
96
 
99
- for (const line of readFileSync(fp, "utf8").split("\n")) {
100
- if (!line || line.startsWith("#")) continue
101
- const cols = line.split("\t")
97
+ // `header: false` — the file's header IS a `#` comment, so it falls out with the other comments
98
+ // rather than being consumed as column names.
99
+ for await (const cols of TSVSpliterator.fromAsync(fp, { header: false })) {
100
+ if (cols[0]?.startsWith("#")) continue
102
101
 
103
102
  // ISO(0), ISO3(1), iso-numeric(2), fips(3), Country(4), ...
104
103
  if (cols[0] && cols[4]) {
@@ -118,85 +117,78 @@ export function createGeonamesAdapter(): CorpusAdapter {
118
117
 
119
118
  async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
120
119
  const dir = dirname(opts.inputPath)
121
- const admin1 = loadAdmin1(dir)
122
- const countries = loadCountries(dir)
123
-
124
- const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
120
+ const admin1 = await loadAdmin1(dir)
121
+ const countries = await loadCountries(dir)
125
122
 
126
- const parser = stream.pipe(
127
- csvParse({ delimiter: "\t", quote: false, relax_column_count: true, skip_empty_lines: true })
128
- )
123
+ // `header: false` — the per-country dump is headerless.
124
+ const rows = TSVSpliterator.fromAsync(opts.inputPath, { header: false })
129
125
 
130
126
  let emitted = 0
131
127
 
132
- try {
133
- for await (const rec of parser as AsyncIterable<string[]>) {
134
- if (opts.signal?.aborted) break
128
+ for await (const rec of rows) {
129
+ if (opts.signal?.aborted) break
135
130
 
136
- if (opts.limit !== undefined && emitted >= opts.limit) break
131
+ if (opts.limit !== undefined && emitted >= opts.limit) break
137
132
 
138
- if (rec[COL.featureClass] !== "P") continue
133
+ if (rec[COL.featureClass] !== "P") continue
139
134
 
140
- if (NON_CURRENT_PPL.has(rec[COL.featureCode] ?? "")) continue
135
+ if (NON_CURRENT_PPL.has(rec[COL.featureCode] ?? "")) continue
141
136
 
142
- const cc = (rec[COL.country] ?? "").trim()
137
+ const cc = (rec[COL.country] ?? "").trim()
143
138
 
144
- if (!cc) continue
139
+ if (!cc) continue
145
140
 
146
- if (opts.country && cc !== opts.country) continue
141
+ if (opts.country && cc !== opts.country) continue
147
142
 
148
- const locality = (rec[COL.name] ?? "").trim()
143
+ const locality = (rec[COL.name] ?? "").trim()
149
144
 
150
- if (!locality) continue
151
- const geonameid = (rec[COL.geonameid] ?? "").trim()
152
- const region = admin1.get(`${cc}.${(rec[COL.admin1] ?? "").trim()}`)
153
- const country = countries.get(cc)
145
+ if (!locality) continue
146
+ const geonameid = (rec[COL.geonameid] ?? "").trim()
147
+ const region = admin1.get(`${cc}.${(rec[COL.admin1] ?? "").trim()}`)
148
+ const country = countries.get(cc)
154
149
 
155
- // Two hierarchy variants (domestic + international order) — but only emit the
156
- // distinct ones the available names support.
157
- const variants: Array<{ slot: string; comp: CanonicalRow["components"]; raw: string }> = []
150
+ // Two hierarchy variants (domestic + international order) — but only emit the
151
+ // distinct ones the available names support.
152
+ const variants: Array<{ slot: string; comp: CanonicalRow["components"]; raw: string }> = []
158
153
 
159
- if (region) {
160
- variants.push({ slot: "lr", comp: { locality, region }, raw: `${locality}, ${region}` })
154
+ if (region) {
155
+ variants.push({ slot: "lr", comp: { locality, region }, raw: `${locality}, ${region}` })
161
156
 
162
- if (country) {
163
- variants.push({
164
- slot: "lrc",
165
- comp: { locality, region, country },
166
- raw: `${locality}, ${region}, ${country}`,
167
- })
168
- }
169
- } else if (country) {
170
- variants.push({ slot: "lc", comp: { locality, country }, raw: `${locality}, ${country}` })
171
- } else {
172
- variants.push({ slot: "l", comp: { locality }, raw: locality })
157
+ if (country) {
158
+ variants.push({
159
+ slot: "lrc",
160
+ comp: { locality, region, country },
161
+ raw: `${locality}, ${region}, ${country}`,
162
+ })
173
163
  }
164
+ } else if (country) {
165
+ variants.push({ slot: "lc", comp: { locality, country }, raw: `${locality}, ${country}` })
166
+ } else {
167
+ variants.push({ slot: "l", comp: { locality }, raw: locality })
168
+ }
174
169
 
175
- for (const v of variants) {
176
- if (opts.limit !== undefined && emitted >= opts.limit) break
177
- const aligned = reconcileComponents(v.comp, v.raw)
178
-
179
- if (!Object.keys(aligned).length) continue
180
-
181
- const sourceID = geonameid
182
- ? `${GEONAMES_ADAPTER_ID}-${geonameid}-${v.slot}`
183
- : stableSourceID(GEONAMES_ADAPTER_ID, aligned)
184
-
185
- yield {
186
- raw: v.raw,
187
- components: aligned,
188
- country: cc,
189
- source: GEONAMES_ADAPTER_ID,
190
- source_id: sourceID,
191
- corpus_version: "",
192
- license: GEONAMES_DEFAULT_LICENSE,
193
- }
194
-
195
- emitted++
170
+ for (const v of variants) {
171
+ if (opts.limit !== undefined && emitted >= opts.limit) break
172
+ const aligned = reconcileComponents(v.comp, v.raw)
173
+
174
+ if (!Object.keys(aligned).length) continue
175
+
176
+ const sourceID = geonameid
177
+ ? `${GEONAMES_ADAPTER_ID}-${geonameid}-${v.slot}`
178
+ : stableSourceID(GEONAMES_ADAPTER_ID, aligned)
179
+
180
+ yield {
181
+ raw: v.raw,
182
+ components: aligned,
183
+ country: cc,
184
+ source: GEONAMES_ADAPTER_ID,
185
+ source_id: sourceID,
186
+ corpus_version: "",
187
+ license: GEONAMES_DEFAULT_LICENSE,
196
188
  }
189
+
190
+ emitted++
197
191
  }
198
- } finally {
199
- stream.destroy()
200
192
  }
201
193
  },
202
194
  }
@@ -23,9 +23,7 @@
23
23
  * `"CC-BY-4.0"` per row (attribute "GeoNames").
24
24
  */
25
25
 
26
- import { createReadStream } from "node:fs"
27
-
28
- import { parse as csvParse } from "csv-parse"
26
+ import { TSVSpliterator } from "spliterator"
29
27
 
30
28
  import { stableSourceID } from "../../adapter.ts"
31
29
  import { reconcileComponents } from "../../format.ts"
@@ -55,67 +53,61 @@ export function createGeonamesPostalAdapter(): CorpusAdapter {
55
53
  "GeoNames postal codes (CC-BY-4.0) — multi-locale postcode→locality→region, names inline; international postcode-first order.",
56
54
 
57
55
  async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
58
- const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
59
-
60
- const parser = stream.pipe(
61
- csvParse({ delimiter: "\t", quote: false, relax_column_count: true, skip_empty_lines: true })
62
- )
56
+ // `header: false` — the GeoNames postal dump is headerless, and the spliterator would
57
+ // otherwise consume row 1 as column names and lose its first postcode.
58
+ const rows = TSVSpliterator.fromAsync(opts.inputPath, { header: false })
63
59
 
64
60
  let emitted = 0
65
61
 
66
- try {
67
- for await (const rec of parser as AsyncIterable<string[]>) {
68
- if (opts.signal?.aborted) break
69
-
70
- if (opts.limit !== undefined && emitted >= opts.limit) break
71
-
72
- const cc = (rec[COL.country] ?? "").trim()
62
+ for await (const rec of rows as AsyncIterable<string[]>) {
63
+ if (opts.signal?.aborted) break
73
64
 
74
- if (!cc) continue
65
+ if (opts.limit !== undefined && emitted >= opts.limit) break
75
66
 
76
- if (opts.country && cc !== opts.country) continue
67
+ const cc = (rec[COL.country] ?? "").trim()
77
68
 
78
- const postcode = (rec[COL.postcode] ?? "").trim()
79
- const locality = (rec[COL.place] ?? "").trim()
69
+ if (!cc) continue
80
70
 
81
- if (!postcode || !locality) continue
82
- const region = (rec[COL.admin1Name] ?? "").trim()
71
+ if (opts.country && cc !== opts.country) continue
83
72
 
84
- // Postcode-first (international) variants. Skip the region variant when admin1 just
85
- // repeats the place (common for city-states / micro-admin) to avoid "X X" noise.
86
- const variants: Array<{ slot: string; comp: CanonicalRow["components"]; raw: string }> = [
87
- { slot: "pl", comp: { postcode, locality }, raw: `${postcode} ${locality}` },
88
- ]
73
+ const postcode = (rec[COL.postcode] ?? "").trim()
74
+ const locality = (rec[COL.place] ?? "").trim()
89
75
 
90
- if (region && region.toLowerCase() !== locality.toLowerCase()) {
91
- variants.push({
92
- slot: "plr",
93
- comp: { postcode, locality, region },
94
- raw: `${postcode} ${locality}, ${region}`,
95
- })
96
- }
97
-
98
- for (const v of variants) {
99
- if (opts.limit !== undefined && emitted >= opts.limit) break
100
- const aligned = reconcileComponents(v.comp, v.raw)
76
+ if (!postcode || !locality) continue
77
+ const region = (rec[COL.admin1Name] ?? "").trim()
101
78
 
102
- if (Object.keys(aligned).length < 2) continue
79
+ // Postcode-first (international) variants. Skip the region variant when admin1 just
80
+ // repeats the place (common for city-states / micro-admin) to avoid "X X" noise.
81
+ const variants: Array<{ slot: string; comp: CanonicalRow["components"]; raw: string }> = [
82
+ { slot: "pl", comp: { postcode, locality }, raw: `${postcode} ${locality}` },
83
+ ]
103
84
 
104
- yield {
105
- raw: v.raw,
106
- components: aligned,
107
- country: cc,
108
- source: GEONAMES_POSTAL_ADAPTER_ID,
109
- source_id: `${stableSourceID(GEONAMES_POSTAL_ADAPTER_ID, aligned)}-${v.slot}`,
110
- corpus_version: "",
111
- license: GEONAMES_POSTAL_DEFAULT_LICENSE,
112
- }
85
+ if (region && region.toLowerCase() !== locality.toLowerCase()) {
86
+ variants.push({
87
+ slot: "plr",
88
+ comp: { postcode, locality, region },
89
+ raw: `${postcode} ${locality}, ${region}`,
90
+ })
91
+ }
113
92
 
114
- emitted++
93
+ for (const v of variants) {
94
+ if (opts.limit !== undefined && emitted >= opts.limit) break
95
+ const aligned = reconcileComponents(v.comp, v.raw)
96
+
97
+ if (Object.keys(aligned).length < 2) continue
98
+
99
+ yield {
100
+ raw: v.raw,
101
+ components: aligned,
102
+ country: cc,
103
+ source: GEONAMES_POSTAL_ADAPTER_ID,
104
+ source_id: `${stableSourceID(GEONAMES_POSTAL_ADAPTER_ID, aligned)}-${v.slot}`,
105
+ corpus_version: "",
106
+ license: GEONAMES_POSTAL_DEFAULT_LICENSE,
115
107
  }
108
+
109
+ emitted++
116
110
  }
117
- } finally {
118
- stream.destroy()
119
111
  }
120
112
  },
121
113
  }
@@ -28,6 +28,7 @@
28
28
  * line). Open G-NAF licence — attribute "Geoscape Australia".
29
29
  */
30
30
 
31
+ import { tryParsingJSON } from "@mailwoman/core/objects"
31
32
  import { TextSpliterator } from "spliterator"
32
33
 
33
34
  import { stableSourceID } from "../../adapter.ts"
@@ -87,7 +88,7 @@ export function createGNAFAdapter(): CorpusAdapter {
87
88
 
88
89
  // Input is the assembled component JSONL (one tuple per line). TextSpliterator auto-disposes on
89
90
  // loop completion and on an early `break` (abort / limit), so the old explicit handle teardown is
90
- // gone; JSON.parse tolerates a trailing CR on CRLF sources and the `!line.trim()` guard skips blanks.
91
+ // gone; the parse tolerates a trailing CR on CRLF sources and the `!line.trim()` guard skips blanks.
91
92
  // The render order rotates (i % 3), matching v1.9.1's rerender.
92
93
  for await (const line of TextSpliterator.fromAsync(opts.inputPath)) {
93
94
  if (opts.signal?.aborted) break
@@ -96,13 +97,9 @@ export function createGNAFAdapter(): CorpusAdapter {
96
97
 
97
98
  if (!line.trim()) continue
98
99
 
99
- let t: GNAFTuple
100
+ const t = tryParsingJSON<GNAFTuple>(line)
100
101
 
101
- try {
102
- t = JSON.parse(line) as GNAFTuple
103
- } catch {
104
- continue
105
- }
102
+ if (t === null) continue
106
103
 
107
104
  if (!t.house_number || !t.street || !t.locality || !t.postcode) continue
108
105
 
@@ -30,7 +30,8 @@ import { createWriteStream } from "node:fs"
30
30
  import { readdir, readFile } from "node:fs/promises"
31
31
  import { join } from "node:path"
32
32
 
33
- import { PSVSpliterator } from "spliterator"
33
+ import { tryParsingJSON } from "@mailwoman/core/objects"
34
+ import { PSVSpliterator, TextSpliterator } from "spliterator"
34
35
 
35
36
  export interface GNAFAssembleOptions {
36
37
  /**
@@ -109,19 +110,14 @@ async function loadMap<V>(paths: string[], keyCol: string, pick: (r: Row) => V):
109
110
  */
110
111
  async function loadHoldout(path: string): Promise<Set<string>> {
111
112
  const keys = new Set<string>()
112
- const text = await readFile(path, "utf8")
113
113
 
114
- for (const line of text.split("\n")) {
114
+ for await (const line of TextSpliterator.fromAsync(path)) {
115
115
  if (!line.trim()) continue
116
116
 
117
- try {
118
- const c = (JSON.parse(line) as { components?: Record<string, string> }).components
117
+ const c = tryParsingJSON<{ components?: Record<string, string> }>(line)?.components
119
118
 
120
- if (c?.street && c?.locality && c?.postcode) {
121
- keys.add(gnafHoldoutKey(c.street, c.locality, c.postcode))
122
- }
123
- } catch {
124
- /* skip malformed */
119
+ if (c?.street && c?.locality && c?.postcode) {
120
+ keys.add(gnafHoldoutKey(c.street, c.locality, c.postcode))
125
121
  }
126
122
  }
127
123