@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
@@ -20,9 +20,9 @@
20
20
  * locality-hash holdout (hash%10===0).
21
21
  *
22
22
  * Prerequisites (read once, before the generation loop — these do NOT consume `random`): the cached
23
- * OA zips in /tmp/oa-cache, the GeoNames Canada dump at /tmp/geonames-cache/CA.zip, and the
24
- * GeoNames POSTAL-CODE dumps for AU/NZ (/tmp/geonames-cache/{AU,NZ}-postal.zip). See the legacy
25
- * script header for the exact curl commands.
23
+ * OA zips in `$MAILWOMAN_DATA_ROOT/oa-cache`, the GeoNames Canada dump at
24
+ * `$MAILWOMAN_DATA_ROOT/geonames/CA.zip`, and the GeoNames POSTAL-CODE dumps for AU/NZ
25
+ * (`$MAILWOMAN_DATA_ROOT/geonames-postal/{AU,NZ}.zip`).
26
26
  *
27
27
  * Byte-fidelity: the legacy script seeded its own mulberry32 from `--seed`
28
28
  * (`mulberry32(opts.seed)`); this recipe re-creates the SAME generator
@@ -35,8 +35,10 @@ import { spawnSync } from "node:child_process"
35
35
  import { isAuDeliveryService, isAuPostcode, isAuStateAbbreviation } from "@mailwoman/codex/au"
36
36
  import { FSA_LETTER_TO_PROVINCE, normalizeCaPostalCode } from "@mailwoman/codex/ca"
37
37
  import { isCedex } from "@mailwoman/codex/fr"
38
- import { isNzDeliveryService, isNzPostcode } from "@mailwoman/codex/nz"
38
+ import { isNZDeliveryService, isNZPostcode } from "@mailwoman/codex/nz"
39
39
  import { isPOBox } from "@mailwoman/codex/us"
40
+ import { dataRootPath } from "@mailwoman/core/utils"
41
+ import { TextSpliterator } from "spliterator"
40
42
 
41
43
  import { alignRow } from "../align.ts"
42
44
  import {
@@ -45,7 +47,7 @@ import {
45
47
  synthesizeMilitaryPoBoxRow,
46
48
  type LocaleTemplate,
47
49
  } from "../synthesize-po-box.ts"
48
- import { makeMulberry32, shardSourceID, type CanonicalShardRow, type ShardRecipe } from "./scaffold.ts"
50
+ import { makeMulberry32, readCSVRecords, shardSourceID, type CanonicalShardRow, type ShardRecipe } from "./scaffold.ts"
49
51
 
50
52
  // ── Base-skeleton sources ────────────────────────────────────────────────────────────────────────
51
53
  // Same OA cache as the unit/affix shards. US train = every NON-Vermont state; US eval = Vermont (the
@@ -58,20 +60,29 @@ import { makeMulberry32, shardSourceID, type CanonicalShardRow, type ShardRecipe
58
60
  extracted as named constants above. */
59
61
 
60
62
  const US_TRAIN_SOURCES = [
61
- { zip: "/tmp/oa-cache/us__ca__berkeley.zip", csv: "us/ca/berkeley.csv", region: "CA" },
62
- { zip: "/tmp/oa-cache/us__ca__marin.zip", csv: "us/ca/marin.csv", region: "CA" },
63
- { zip: "/tmp/oa-cache/us__dc__statewide.zip", csv: "us/dc/statewide.csv", region: "DC" },
64
- { zip: "/tmp/oa-cache/us__ia__statewide.zip", csv: "us/ia/statewide.csv", region: "IA" },
65
- { zip: "/tmp/oa-cache/us__il__cook.zip", csv: "us/il/cook.csv", region: "IL" },
66
- { zip: "/tmp/oa-cache/us__mt__statewide.zip", csv: "us/mt/statewide.csv", region: "MT" },
67
- { zip: "/tmp/oa-cache/us__sd__statewide.zip", csv: "us/sd/statewide.csv", region: "SD" },
63
+ { zip: dataRootPath("oa-cache", "us__ca__berkeley.zip"), csv: "us/ca/berkeley.csv", region: "CA" },
64
+ { zip: dataRootPath("oa-cache", "us__ca__marin.zip"), csv: "us/ca/marin.csv", region: "CA" },
65
+ { zip: dataRootPath("oa-cache", "us__dc__statewide.zip"), csv: "us/dc/statewide.csv", region: "DC" },
66
+ { zip: dataRootPath("oa-cache", "us__ia__statewide.zip"), csv: "us/ia/statewide.csv", region: "IA" },
67
+ { zip: dataRootPath("oa-cache", "us__il__cook.zip"), csv: "us/il/cook.csv", region: "IL" },
68
+ { zip: dataRootPath("oa-cache", "us__mt__statewide.zip"), csv: "us/mt/statewide.csv", region: "MT" },
69
+ { zip: dataRootPath("oa-cache", "us__sd__statewide.zip"), csv: "us/sd/statewide.csv", region: "SD" },
68
70
  ]
69
71
 
70
- const US_EVAL_SOURCE = { zip: "/tmp/oa-cache/us__vt__statewide.zip", csv: "us/vt/statewide.csv", region: "VT" }
71
- const FR_SOURCE = { zip: "/tmp/oa-cache/fr__countrywide.zip", csv: "fr/countrywide.csv" }
72
- const GEONAMES_CA = "/tmp/geonames-cache/CA.zip"
73
- const GEONAMES_POSTAL_AU = { zip: "/tmp/geonames-cache/AU-postal.zip", txt: "AU.txt" }
74
- const GEONAMES_POSTAL_NZ = { zip: "/tmp/geonames-cache/NZ-postal.zip", txt: "NZ.txt" }
72
+ const US_EVAL_SOURCE = {
73
+ zip: dataRootPath("oa-cache", "us__vt__statewide.zip"),
74
+ csv: "us/vt/statewide.csv",
75
+ region: "VT",
76
+ }
77
+
78
+ const FR_SOURCE = { zip: dataRootPath("oa-cache", "fr__countrywide.zip"), csv: "fr/countrywide.csv" }
79
+ // GeoNames has two per-country exports with different schemas, and this recipe reads both. The MAIN
80
+ // dump (feature class + population columns) lives under `geonames/`; the POSTAL-CODE dump lives
81
+ // under `geonames-postal/`, where the directory carries the "postal" distinction the filename used
82
+ // to.
83
+ const GEONAMES_CA = dataRootPath("geonames", "CA.zip")
84
+ const GEONAMES_POSTAL_AU = { zip: dataRootPath("geonames-postal", "AU.zip"), txt: "AU.txt" }
85
+ const GEONAMES_POSTAL_NZ = { zip: dataRootPath("geonames-postal", "NZ.zip"), txt: "NZ.txt" }
75
86
 
76
87
  /**
77
88
  * ── Surface vocabulary (codex + corpus templates — see the header) ──────────────────────────────.
@@ -172,20 +183,20 @@ interface USTuple {
172
183
  postcode: string
173
184
  }
174
185
 
175
- interface FrTuple {
186
+ interface FRTuple {
176
187
  house_number: string
177
188
  street: string
178
189
  locality: string
179
190
  postcode: string
180
191
  }
181
192
 
182
- interface AuTuple {
193
+ interface AUTuple {
183
194
  locality: string
184
195
  region: string
185
196
  postcode: string
186
197
  }
187
198
 
188
- interface NzTuple {
199
+ interface NZTuple {
189
200
  locality: string
190
201
  postcode: string
191
202
  }
@@ -214,44 +225,6 @@ function localityHash(name: string): number {
214
225
 
215
226
  const isHoldoutLocality = (name: string): boolean => localityHash(name) % 10 === 0
216
227
 
217
- /**
218
- * Minimal RFC-4180-ish splitter (handles quoted fields).
219
- */
220
- function splitCSV(line: string): string[] {
221
- const out: string[] = []
222
- let cur = ""
223
- let inQ = false
224
-
225
- for (let i = 0; i < line.length; i++) {
226
- const c = line[i]!
227
-
228
- if (inQ) {
229
- if (c === '"') {
230
- if (line[i + 1] === '"') {
231
- cur += '"'
232
-
233
- i++
234
- } else {
235
- inQ = false
236
- }
237
- } else {
238
- cur += c
239
- }
240
- } else if (c === '"') {
241
- inQ = true
242
- } else if (c === ",") {
243
- out.push(cur)
244
- cur = ""
245
- } else {
246
- cur += c
247
- }
248
- }
249
-
250
- out.push(cur)
251
-
252
- return out
253
- }
254
-
255
228
  const MAX_LOCALITY_LENGTH = 40
256
229
 
257
230
  const cleanLocality = (loc: string) =>
@@ -269,38 +242,24 @@ function readUsTuples(source: { zip: string; csv: string; region: string }): UST
269
242
  return []
270
243
  }
271
244
 
272
- const lines = r.stdout.toString("utf8").split(/\r?\n/)
273
-
274
- if (lines.length < 2) return []
275
- const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
276
- const idx = (name: string) => header.indexOf(name)
277
-
278
- const iNum = idx("number"),
279
- iStreet = idx("street"),
280
- iCity = idx("city"),
281
- iPost = idx("postcode")
282
-
283
- const get = (cells: string[], i: number) => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "")
284
245
  const tuples: USTuple[] = []
285
246
  const seen = new Set<string>()
286
247
 
287
- for (let li = 1; li < lines.length; li++) {
288
- if (!lines[li]) continue
289
- const cells = splitCSV(lines[li]!)
290
- const locality = get(cells, iCity)
248
+ for (const row of readCSVRecords(r.stdout)) {
249
+ const locality = row.city ?? ""
291
250
 
292
251
  if (!cleanLocality(locality)) continue
293
252
  const key = locality.toLowerCase()
294
253
 
295
- const street = get(cells, iStreet),
296
- house_number = get(cells, iNum)
254
+ const street = row.street ?? "",
255
+ house_number = row.number ?? ""
297
256
 
298
257
  // One tuple per (locality, street) pair keeps the pool varied without ballooning memory.
299
258
  const pairKey = `${key}|${street}`.toLowerCase()
300
259
 
301
260
  if (seen.has(pairKey)) continue
302
261
  seen.add(pairKey)
303
- tuples.push({ house_number, street, locality, region: source.region, postcode: get(cells, iPost) })
262
+ tuples.push({ house_number, street, locality, region: source.region, postcode: row.postcode ?? "" })
304
263
  }
305
264
 
306
265
  return tuples
@@ -308,10 +267,13 @@ function readUsTuples(source: { zip: string; csv: string; region: string }): UST
308
267
 
309
268
  /**
310
269
  * Stride-sampled FR tuples (number/street/city/postcode). The countrywide CSV is 2.5 GB and insee-ordered; `awk NR%K`
311
- * strides the whole country instead of reading one département. Quoted commas survive because awk only FILTERS lines —
312
- * parsing stays in splitCSV.
270
+ * strides the whole country instead of reading one département.
271
+ *
272
+ * The stride counts PHYSICAL lines, so a record carrying a newline inside a quoted field is two lines to awk and can be
273
+ * cut in half by the stride. That is a sampling artefact of the awk pre-filter, not of the parse: whichever lines
274
+ * survive are re-assembled into records by {@link readCSVRecords}, and a halved record fails the field checks below.
313
275
  */
314
- function readFrTuples(limit: number): FrTuple[] {
276
+ function readFrTuples(limit: number): FRTuple[] {
315
277
  const r = spawnSync(
316
278
  "bash",
317
279
  ["-c", `unzip -p "${FR_SOURCE.zip}" "${FR_SOURCE.csv}" | awk 'NR==1 || NR%211==3' | head -n ${limit + 1}`],
@@ -324,29 +286,14 @@ function readFrTuples(limit: number): FrTuple[] {
324
286
  return []
325
287
  }
326
288
 
327
- const lines = r.stdout.toString("utf8").split(/\r?\n/)
328
-
329
- if (lines.length < 2) return []
330
- const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
331
- const idx = (n: string) => header.indexOf(n)
332
-
333
- const iNum = idx("number"),
334
- iStreet = idx("street"),
335
- iCity = idx("city"),
336
- iPost = idx("postcode")
337
-
338
- const get = (cells: string[], i: number) => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "")
339
- const tuples: FrTuple[] = []
289
+ const tuples: FRTuple[] = []
340
290
  const seen = new Set<string>()
341
291
 
342
- for (let li = 1; li < lines.length; li++) {
343
- if (!lines[li]) continue
344
- const cells = splitCSV(lines[li]!)
345
-
346
- const locality = get(cells, iCity),
347
- postcode = get(cells, iPost),
348
- street = get(cells, iStreet),
349
- house_number = get(cells, iNum)
292
+ for (const row of readCSVRecords(r.stdout)) {
293
+ const locality = row.city ?? "",
294
+ postcode = row.postcode ?? "",
295
+ street = row.street ?? "",
296
+ house_number = row.number ?? ""
350
297
 
351
298
  if (!cleanLocality(locality) || !/^\d{5}$/.test(postcode) || !street || !house_number) continue
352
299
  const key = `${locality}|${street}`.toLowerCase()
@@ -379,7 +326,7 @@ function readCaLocalities(admin1: string): string[] {
379
326
  return []
380
327
  }
381
328
 
382
- return [...new Set(r.stdout.split("\n").filter(cleanLocality))]
329
+ return [...new Set([...TextSpliterator.from(r.stdout)].filter(cleanLocality))]
383
330
  }
384
331
 
385
332
  /**
@@ -392,7 +339,7 @@ function readCaLocalities(admin1: string): string[] {
392
339
  function readPostalTuples(
393
340
  source: { zip: string; txt: string },
394
341
  opts: { withState: boolean }
395
- ): Array<AuTuple | NzTuple> {
342
+ ): Array<AUTuple | NZTuple> {
396
343
  const r = spawnSync("unzip", ["-p", source.zip, source.txt], { maxBuffer: 1024 * 1024 * 64, encoding: "utf8" })
397
344
 
398
345
  if (r.status !== 0) {
@@ -401,12 +348,14 @@ function readPostalTuples(
401
348
  return []
402
349
  }
403
350
 
404
- const tuples: Array<AuTuple | NzTuple> = []
351
+ const tuples: Array<AUTuple | NZTuple> = []
405
352
  const seen = new Set<string>()
406
- const validPostcode = opts.withState ? isAuPostcode : isNzPostcode
353
+ const validPostcode = opts.withState ? isAuPostcode : isNZPostcode
407
354
 
408
- for (const line of r.stdout.split("\n")) {
355
+ for (const line of TextSpliterator.from(r.stdout)) {
409
356
  if (!line) continue
357
+
358
+ // oxlint-disable-next-line mailwoman/prefer-spliterator -- One row already streamed off the line above.
410
359
  const cols = line.split("\t")
411
360
  const postcode = (cols[1] ?? "").trim()
412
361
  const locality = (cols[2] ?? "").trim()
@@ -514,7 +463,7 @@ function makeCedex(random: () => number): string {
514
463
  /**
515
464
  * Compose an AU or NZ delivery-service phrase from the codex-sourced leaders. Same contract as makePoBoxPhrase: a
516
465
  * phrase built from a codex-known designator and a clean id must round-trip the codex matcher (isAuDeliveryService /
517
- * isNzDeliveryService) — a failure is a generation bug, loud. Noisy ids (commas / embedded spaces) are corpus-designed
466
+ * isNZDeliveryService) — a failure is a generation bug, loud. Noisy ids (commas / embedded spaces) are corpus-designed
518
467
  * adversarial forms, exempt.
519
468
  */
520
469
  function makeAuNzPoBoxPhrase(
@@ -532,14 +481,14 @@ function makeAuNzPoBoxPhrase(
532
481
  let num = maybeNoisifyBoxNumber(pickBoxNumber(random), random)
533
482
 
534
483
  // NZ CMB identifiers are alpha-led per the ADV358 example ("CMB B99").
535
- if (leader === "CMB" && validate === isNzDeliveryService) {
484
+ if (leader === "CMB" && validate === isNZDeliveryService) {
536
485
  num = `B${num}`
537
486
  }
538
487
 
539
488
  const phrase = `${caseDial(random, leader)} ${num}`
540
489
  // The "clean id" shape differs per system: ADV358 identifiers carry no separators at all, the AU
541
490
  // AMAS id (like the US one) tolerates dashes. Noisy ids outside the clean shape are exempt.
542
- const cleanID = validate === isNzDeliveryService ? /^[\dA-Za-z]+$/ : /^[\dA-Za-z][\dA-Za-z-]*$/
491
+ const cleanID = validate === isNZDeliveryService ? /^[\dA-Za-z]+$/ : /^[\dA-Za-z][\dA-Za-z-]*$/
543
492
 
544
493
  if (cleanID.test(num) && !validate(phrase)) {
545
494
  throw new Error(`generated a phrase the codex matcher rejects: "${phrase}"`)
@@ -614,7 +563,7 @@ function renderPmbUs(random: () => number, t: USTuple): Rendered {
614
563
  return { fmt: "pmb-bare", raw: `${road} ${phrase}`, components: { house_number: hn, street, po_box: phrase } }
615
564
  }
616
565
 
617
- function renderBpFr(random: () => number, t: FrTuple): Rendered {
566
+ function renderBpFr(random: () => number, t: FRTuple): Rendered {
618
567
  const phrase = makePoBoxPhrase(random, FR_LEADERS)
619
568
  const { locality, postcode: pc } = t
620
569
  const upper = random() < 0.5
@@ -651,7 +600,7 @@ function renderBpFr(random: () => number, t: FrTuple): Rendered {
651
600
  }
652
601
  }
653
602
 
654
- function renderCedexFr(random: () => number, t: FrTuple): Rendered {
603
+ function renderCedexFr(random: () => number, t: FRTuple): Rendered {
655
604
  const cedex = makeCedex(random)
656
605
  const { house_number: hn, street, locality, postcode: pc } = t
657
606
  const loc = random() < 0.6 ? locality.toUpperCase() : locality
@@ -703,7 +652,7 @@ function renderCaEn(random: () => number, loc: string): Rendered {
703
652
  return { fmt: "ca-en-bare", raw: phrase, components: { po_box: phrase } }
704
653
  }
705
654
 
706
- function renderAuPoBox(random: () => number, t: AuTuple): Rendered {
655
+ function renderAUPoBox(random: () => number, t: AUTuple): Rendered {
707
656
  const phrase = makeAuNzPoBoxPhrase(random, AU_LEADERS_CURRENT, AU_LEADERS_LEGACY, isAuDeliveryService)
708
657
  const { locality, region: reg, postcode: pc } = t
709
658
  const r = random()
@@ -737,8 +686,8 @@ function renderAuPoBox(random: () => number, t: AuTuple): Rendered {
737
686
  return { fmt: "au-venue", raw: `${v}, ${phrase}, ${loc} ${reg} ${pc}`, components: { venue: v, ...base } }
738
687
  }
739
688
 
740
- function renderNzPoBox(random: () => number, t: NzTuple): Rendered {
741
- const phrase = makeAuNzPoBoxPhrase(random, NZ_LEADERS_COMMON, NZ_LEADERS_RARE, isNzDeliveryService)
689
+ function renderNZPoBox(random: () => number, t: NZTuple): Rendered {
690
+ const phrase = makeAuNzPoBoxPhrase(random, NZ_LEADERS_COMMON, NZ_LEADERS_RARE, isNZDeliveryService)
742
691
  const { locality, postcode: pc } = t
743
692
  // NZ addresses are written mixed-case ("PO Box 4099, Timaru 7942") — no region line (ADV358).
744
693
  const base = { po_box: phrase, locality, postcode: pc }
@@ -818,8 +767,8 @@ export const poBoxCedexRecipe: ShardRecipe = {
818
767
  console.error(` GeoNames CA: QC ${qcAll.length}→${qcPool.length}, ON ${onAll.length}→${onPool.length}`)
819
768
 
820
769
  // AU/NZ pools: same stable locality-hash holdout as FR/CA.
821
- const auAll = readPostalTuples(GEONAMES_POSTAL_AU, { withState: true }) as AuTuple[]
822
- const nzAll = readPostalTuples(GEONAMES_POSTAL_NZ, { withState: false }) as NzTuple[]
770
+ const auAll = readPostalTuples(GEONAMES_POSTAL_AU, { withState: true }) as AUTuple[]
771
+ const nzAll = readPostalTuples(GEONAMES_POSTAL_NZ, { withState: false }) as NZTuple[]
823
772
  const auPool = auAll.filter((t) => isHoldoutLocality(t.locality) === opts.golden)
824
773
  const nzPool = nzAll.filter((t) => isHoldoutLocality(t.locality) === opts.golden)
825
774
 
@@ -827,7 +776,7 @@ export const poBoxCedexRecipe: ShardRecipe = {
827
776
 
828
777
  if (!usPool.length || !frPool.length || !qcPool.length || !onPool.length || !auPool.length || !nzPool.length) {
829
778
  throw new Error(
830
- "A base pool is empty — check /tmp/oa-cache and /tmp/geonames-cache (CA.zip, AU-postal.zip, NZ-postal.zip)."
779
+ `A base pool is empty — check ${dataRootPath("oa-cache")}, ${GEONAMES_CA}, ${GEONAMES_POSTAL_AU.zip}, and ${GEONAMES_POSTAL_NZ.zip}.`
831
780
  )
832
781
  }
833
782
 
@@ -877,11 +826,11 @@ export const poBoxCedexRecipe: ShardRecipe = {
877
826
  country = "CA"
878
827
  locale = "fr-CA"
879
828
  } else if (cls === "po-box-au") {
880
- rendered = renderAuPoBox(random, pick(random, auPool))
829
+ rendered = renderAUPoBox(random, pick(random, auPool))
881
830
  country = "AU"
882
831
  locale = "en-AU"
883
832
  } else if (cls === "po-box-nz") {
884
- rendered = renderNzPoBox(random, pick(random, nzPool))
833
+ rendered = renderNZPoBox(random, pick(random, nzPool))
885
834
  country = "NZ"
886
835
  locale = "en-NZ"
887
836
  } else if (cls === "po-box-us-military") {
@@ -9,6 +9,9 @@
9
9
  * only its synthesis + filter; the `mailwoman corpus shard <recipe>` command supplies the I/O.
10
10
  */
11
11
 
12
+ import { tryParsingJSON } from "@mailwoman/core/objects"
13
+ import { CSVSpliterator } from "spliterator"
14
+
12
15
  import { stableSourceID } from "../adapter.ts"
13
16
  import { alignRow } from "../align.ts"
14
17
 
@@ -33,38 +36,65 @@ export interface ShardTuple {
33
36
  }
34
37
 
35
38
  /**
36
- * The deterministic LCG (`s = s*1664525 + 1013904223 mod 2^32`) the street/po-box/anchor builders used. A recipe whose
37
- * legacy `.mjs` seeded this must create it here so `--seed N` is byte-reproducible.
39
+ * The two seeded generators the legacy `build-*-shard` scripts used, re-exported from their home in
40
+ * `@mailwoman/core/utils` so a recipe keeps importing everything it needs from this one scaffold module.
41
+ *
42
+ * - `makeLcg` (`s = s*1664525 + 1013904223 mod 2^32`) — what the street/po-box/anchor builders seeded.
43
+ * - `makeMulberry32` — what the MAJORITY of them used (german, locale, boundary-stress, unit, fr-order, country-balanced,
44
+ * intersection, fr-admin-split, street-affix, street-bare, po-box-cedex).
45
+ *
46
+ * A recipe must seed the same one its `.mjs` did, the same way it did (usually `seed`, but some derive a per-stream
47
+ * seed), or `--seed N` stops being byte-reproducible.
38
48
  */
39
- export function makeLcg(seed: number): () => number {
40
- let s = seed >>> 0
41
-
42
- return () => {
43
- s = (s * 1_664_525 + 1_013_904_223) % 4_294_967_296
44
-
45
- return s / 4_294_967_296
46
- }
47
- }
49
+ export { makeLcg, mulberry32 as makeMulberry32, makeLcg as makeRandom } from "@mailwoman/core/utils"
48
50
 
49
51
  /**
50
- * Back-compat alias for {@link makeLcg}.
52
+ * One CSV record, keyed by its lower-cased header name, every value trimmed. A column the header does not declare reads
53
+ * as `undefined`; a column the header declares but this record does not reach reads as `""`.
51
54
  */
52
- export const makeRandom = makeLcg
55
+ export type CSVRecord = Record<string, string | undefined>
53
56
 
54
57
  /**
55
- * Mulberry32 — the PRNG the MAJORITY of the legacy `build-*-shard` scripts used (german, locale, boundary-stress, unit,
56
- * fr-order, country-balanced, intersection, fr-admin-split, street-affix, street-bare, po-box-cedex). A recipe must
57
- * seed it EXACTLY as its `.mjs` did (usually `seed`, but some derive a per-stream seed) to stay byte-reproducible.
58
+ * Read an in-memory CSV (an `unzip -p` buffer, say) as header-keyed records.
59
+ *
60
+ * Quote handling spans the ROW split, not only the column split: a newline inside a quoted field belongs to a single
61
+ * record, so the record boundaries can only be found by a scanner that already knows where the quotes are. Find the
62
+ * lines first and unquote afterwards and such a record splits in two — the first half short by however many columns
63
+ * followed the newline, which is how a `street` value comes to be read as `city`.
64
+ *
65
+ * Header names are lower-cased on the way in. `normalizeKeys` will not do it: it leaves an ALL CAPS header alone, and
66
+ * OpenAddresses ships `LON,LAT,NUMBER,STREET` while other extracts ship the same names lower-case. A recipe names its
67
+ * columns in lower case either way.
68
+ *
69
+ * Line breaks inside a value become single spaces. A quote-aware parse is the first thing here able to return a value
70
+ * CONTAINING one — `us/ia/statewide.csv` has 12, all unit designators like `"#2\n#2"` — and every consumer synthesizes
71
+ * one-line address text from these cells with no guard, because until that parse landed no value could carry one.
72
+ * Collapsing keeps the record (the address is fine; the source's line break is not part of it) without emitting a
73
+ * training row with a newline inside it.
74
+ *
75
+ * Only `\r` and `\n`, deliberately — NOT `\s`. Runs of spaces and tabs pass through exactly as the source wrote them
76
+ * (OA's IA extract writes `NORTH`, three spaces, `MAIN STREET`), because those could always appear and every shard
77
+ * built to date contains them. Widening this to `\s+` silently rewrites values on rows with no line break at all.
78
+ * `scaffold.test.ts` pins both halves.
58
79
  */
59
- export function makeMulberry32(seed: number): () => number {
60
- let a = seed >>> 0
80
+ export function* readCSVRecords(source: Uint8Array): Generator<CSVRecord> {
81
+ let header: string[] | null = null
82
+
83
+ // `header: false` keeps the first record in the stream so this reader owns the lower-casing.
84
+ for (const cells of CSVSpliterator.from(source, { header: false, enableQuoteHandling: true })) {
85
+ if (!header) {
86
+ header = cells.map((name) => name.trim().toLowerCase())
87
+
88
+ continue
89
+ }
61
90
 
62
- return () => {
63
- a = (a + 0x6d_2b_79_f5) | 0
64
- let t = Math.imul(a ^ (a >>> 15), 1 | a)
65
- t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t
91
+ const record: CSVRecord = {}
66
92
 
67
- return ((t ^ (t >>> 14)) >>> 0) / 4_294_967_296
93
+ for (let i = 0; i < header.length; i++) {
94
+ record[header[i]!] = (cells[i] ?? "").replaceAll(/[\r\n]+/g, " ").trim()
95
+ }
96
+
97
+ yield record
68
98
  }
69
99
  }
70
100
 
@@ -73,7 +103,7 @@ export function makeMulberry32(seed: number): () => number {
73
103
  */
74
104
  export async function* readTuples(input: string): AsyncGenerator<ShardTuple> {
75
105
  // TextSpliterator (not JSONSpliterator) so a malformed line is SKIPPED, not thrown — the
76
- // per-line try/catch below is the tolerance this reader has always had.
106
+ // per-line tryParsingJSON below is the tolerance this reader has always had.
77
107
  const { TextSpliterator } = await import("spliterator")
78
108
 
79
109
  for await (const line of TextSpliterator.fromAsync(input)) {
@@ -81,10 +111,10 @@ export async function* readTuples(input: string): AsyncGenerator<ShardTuple> {
81
111
 
82
112
  if (!trimmed) continue
83
113
 
84
- try {
85
- yield JSON.parse(trimmed) as ShardTuple
86
- } catch {
87
- // skip malformed line
114
+ const tuple = tryParsingJSON<ShardTuple>(trimmed)
115
+
116
+ if (tuple !== null) {
117
+ yield tuple
88
118
  }
89
119
  }
90
120
  }
@@ -178,6 +208,30 @@ export interface ShardRecipeOpts {
178
208
  * `no-fragment` knob 3: minimum digit count for a number to count as "long" and be boosted. Default 3.
179
209
  */
180
210
  longNumberMinDigits?: number
211
+ /**
212
+ * `sub-venue`: the sub-venue lexicon JSON. Default = the committed `corpus/data/sub-venue-lexicon.json`, resolved
213
+ * through the package manifest so it works from the source tree and from `out/`.
214
+ */
215
+ lexicon?: string
216
+ /**
217
+ * `sub-venue`: directory of `sub-venue-extract` JSONLs, one per region. Default
218
+ * `$MAILWOMAN_DATA_ROOT/sub-venue/extracts`.
219
+ */
220
+ extractsDir?: string
221
+ /**
222
+ * `sub-venue`: the `poi.db` spatial layer, read for the en-US and fr-FR venue + confound pools (the two of poi.db's
223
+ * four countries this shard has legs for). Default `$MAILWOMAN_DATA_ROOT/poi/poi.db`.
224
+ */
225
+ poiDb?: string
226
+ /**
227
+ * `sub-venue`: GB/US/FR address-context tuples JSONL. Default the house-venue v3 tuples
228
+ * (`$MAILWOMAN_DATA_ROOT/corpus/intermediate/house-venue-tuples-v3.jsonl`); DE and ES read OpenAddresses directly.
229
+ */
230
+ subVenueTuples?: string
231
+ /**
232
+ * `sub-venue`: share of emitted rows that are confound NEGATIVES. Default 0.3.
233
+ */
234
+ negativeFraction?: number
181
235
  }
182
236
 
183
237
  /**