@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
@@ -33,11 +33,12 @@
33
33
  import { spawnSync } from "node:child_process"
34
34
 
35
35
  import type { ComponentTag } from "@mailwoman/core/types"
36
+ import { dataRootPath } from "@mailwoman/core/utils"
36
37
 
37
38
  import { stableSourceID } from "../adapter.ts"
38
39
  import { alignRow } from "../align.ts"
39
40
  import type { CanonicalRow } from "../types.ts"
40
- import { makeMulberry32, type ShardRecipe } from "./scaffold.ts"
41
+ import { makeMulberry32, readCSVRecords, type ShardRecipe } from "./scaffold.ts"
41
42
 
42
43
  /* oxlint-disable sister-software/no-unnamed-threshold -- the bare decimals below are weighted-sampler
43
44
  cutoffs, not thresholds: `const r = random()` followed by a cascade of `r < 0.4` branches IS the
@@ -45,7 +46,7 @@ import { makeMulberry32, type ShardRecipe } from "./scaffold.ts"
45
46
  would hide the distribution behind a wall of identifiers. Genuine thresholds in these files are
46
47
  extracted as named constants above. */
47
48
 
48
- const SOURCE = { zip: "/tmp/oa-cache/fr__countrywide.zip", csv: "fr/countrywide.csv" }
49
+ const SOURCE = { zip: dataRootPath("oa-cache", "fr__countrywide.zip"), csv: "fr/countrywide.csv" }
49
50
 
50
51
  /**
51
52
  * Ordinal suffixes used in French house numbers (BAN corpus), to cover the "8 bis" sub-mode.
@@ -71,47 +72,9 @@ interface FrTuple {
71
72
  }
72
73
 
73
74
  /**
74
- * Minimal RFC-4180-ish splitter (handles quoted fields with doubled-quote escaping).
75
- */
76
- function splitCSV(line: string): string[] {
77
- const out: string[] = []
78
- let cur = ""
79
- let inQ = false
80
-
81
- for (let i = 0; i < line.length; i++) {
82
- const c = line[i]
83
-
84
- if (inQ) {
85
- if (c === '"') {
86
- if (line[i + 1] === '"') {
87
- cur += '"'
88
-
89
- i++
90
- } else {
91
- inQ = false
92
- }
93
- } else {
94
- cur += c
95
- }
96
- } else if (c === '"') {
97
- inQ = true
98
- } else if (c === ",") {
99
- out.push(cur)
100
- cur = ""
101
- } else {
102
- cur += c
103
- }
104
- }
105
-
106
- out.push(cur)
107
-
108
- return out
109
- }
110
-
111
- /**
112
- * Stream FR tuples out of the cached OA zip. The countrywide extract is GB-scale; cap with `head` to stay under V8's
113
- * string limit. Only keeps rows with a house_number (the shard's core signal) and a postcode (required for
114
- * reversed-order rendering to be meaningful).
75
+ * Stream FR tuples out of the cached OA zip. The countrywide extract is GB-scale; cap with `head` so the whole file
76
+ * never has to be held in memory. Only keeps rows with a house_number (the shard's core signal) and a postcode
77
+ * (required for reversed-order rendering to be meaningful).
115
78
  */
116
79
  function readTuples(limit: number): FrTuple[] {
117
80
  const maxLines = Math.max(limit * 8, 40_000) + 1
@@ -127,28 +90,15 @@ function readTuples(limit: number): FrTuple[] {
127
90
  return []
128
91
  }
129
92
 
130
- const lines = r.stdout.toString("utf8").split(/\r?\n/)
131
-
132
- if (lines.length < 2) return []
133
- const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
134
- const idx = (name: string): number => header.indexOf(name)
135
-
136
- const iNum = idx("number"),
137
- iStreet = idx("street"),
138
- iCity = idx("city"),
139
- iPost = idx("postcode")
140
-
141
- const get = (cells: string[], i: number): string => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "")
142
93
  const tuples: FrTuple[] = []
143
94
  const seen = new Set<string>()
144
95
 
145
- for (let li = 1; li < lines.length && tuples.length < limit; li++) {
146
- if (!lines[li]) continue
147
- const cells = splitCSV(lines[li]!)
148
- const street = get(cells, iStreet)
149
- const locality = get(cells, iCity)
150
- const house_number = get(cells, iNum)
151
- const postcode = get(cells, iPost)
96
+ for (const row of readCSVRecords(r.stdout)) {
97
+ if (tuples.length >= limit) break
98
+ const street = row.street ?? ""
99
+ const locality = row.city ?? ""
100
+ const house_number = row.number ?? ""
101
+ const postcode = row.postcode ?? ""
152
102
 
153
103
  // Require all four fields: HN is the signal; postcode drives reversed-order variants.
154
104
  if (!street || !locality || !house_number || !postcode) continue
@@ -216,7 +166,7 @@ export const frOrderRecipe: ShardRecipe = {
216
166
  console.error(` ${SOURCE.csv}: ${pool.length} unique tuples (capped read)`)
217
167
 
218
168
  if (!pool.length) {
219
- throw new Error("No FR tuples found — is /tmp/oa-cache/fr__countrywide.zip present?")
169
+ throw new Error(`No FR tuples found — is ${SOURCE.zip} present?`)
220
170
  }
221
171
 
222
172
  let emitted = 0
@@ -17,10 +17,12 @@
17
17
 
18
18
  import { spawnSync } from "node:child_process"
19
19
 
20
+ import { dataRootPath } from "@mailwoman/core/utils"
21
+
20
22
  import { stableSourceID } from "../adapter.ts"
21
23
  import { alignRow } from "../align.ts"
22
24
  import { synthesizeGermanRow, type LocaleBaseTuple } from "../synthesize-german.ts"
23
- import { makeMulberry32, type ShardRecipe } from "./scaffold.ts"
25
+ import { makeMulberry32, readCSVRecords, type ShardRecipe } from "./scaffold.ts"
24
26
 
25
27
  /**
26
28
  * A German OA source (cached zip) + the Bundesland the file covers (OA's REGION column is empty for DE).
@@ -37,48 +39,10 @@ interface GermanSource {
37
39
  * Berlin (a city-state, region==locality); sn/statewide → Sachsen.
38
40
  */
39
41
  const SOURCES: GermanSource[] = [
40
- { zip: "/tmp/oa-cache/de__berlin.zip", csv: "de/berlin.csv", region: "Berlin" },
41
- { zip: "/tmp/oa-cache/de__sn__statewide.zip", csv: "de/sn/statewide.csv", region: "Sachsen" },
42
+ { zip: dataRootPath("oa-cache", "de__berlin.zip"), csv: "de/berlin.csv", region: "Berlin" },
43
+ { zip: dataRootPath("oa-cache", "de__sn__statewide.zip"), csv: "de/sn/statewide.csv", region: "Sachsen" },
42
44
  ]
43
45
 
44
- /**
45
- * Minimal RFC-4180-ish splitter (handles quoted fields).
46
- */
47
- function splitCSV(line: string): string[] {
48
- const out: string[] = []
49
- let cur = ""
50
- let inQ = false
51
-
52
- for (let i = 0; i < line.length; i++) {
53
- const c = line[i]!
54
-
55
- if (inQ) {
56
- if (c === '"') {
57
- if (line[i + 1] === '"') {
58
- cur += '"'
59
-
60
- i++
61
- } else {
62
- inQ = false
63
- }
64
- } else {
65
- cur += c
66
- }
67
- } else if (c === '"') {
68
- inQ = true
69
- } else if (c === ",") {
70
- out.push(cur)
71
- cur = ""
72
- } else {
73
- cur += c
74
- }
75
- }
76
-
77
- out.push(cur)
78
-
79
- return out
80
- }
81
-
82
46
  /**
83
47
  * Stream real German tuples out of a cached OA zip (buffered `unzip -p`).
84
48
  */
@@ -91,35 +55,18 @@ function readGermanTuples(source: GermanSource): LocaleBaseTuple[] {
91
55
  return []
92
56
  }
93
57
 
94
- const lines = r.stdout.toString("utf8").split(/\r?\n/)
95
-
96
- if (lines.length < 2) return []
97
- const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
98
- const idx = (name: string): number => header.indexOf(name)
99
-
100
- const iNum = idx("number"),
101
- iStreet = idx("street"),
102
- iCity = idx("city"),
103
- iRegion = idx("region"),
104
- iPost = idx("postcode")
105
-
106
- const get = (cells: string[], i: number): string => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "")
107
58
  const tuples: LocaleBaseTuple[] = []
108
59
  const seen = new Set<string>()
109
60
 
110
- for (let li = 1; li < lines.length; li++) {
111
- const lineStr = lines[li]
112
-
113
- if (!lineStr) continue
114
- const cells = splitCSV(lineStr)
115
- const street = get(cells, iStreet)
116
- const locality = get(cells, iCity)
61
+ for (const row of readCSVRecords(r.stdout)) {
62
+ const street = row.street ?? ""
63
+ const locality = row.city ?? ""
117
64
 
118
65
  if (!street || !locality) continue
119
- const house_number = get(cells, iNum)
120
- const postcode = get(cells, iPost)
66
+ const house_number = row.number ?? ""
67
+ const postcode = row.postcode ?? ""
121
68
  // OA's REGION column is empty for DE — fall back to the source's Bundesland (set per file).
122
- const region = get(cells, iRegion) || source.region || ""
69
+ const region = row.region || source.region || ""
123
70
  const key = `${house_number}|${street}|${locality}|${postcode}`.toLowerCase()
124
71
 
125
72
  if (seen.has(key)) continue
@@ -165,7 +112,7 @@ export const germanRecipe: ShardRecipe = {
165
112
  }
166
113
 
167
114
  if (!pool.length) {
168
- throw new Error("No German tuples found — are the cached zips present in /tmp/oa-cache?")
115
+ throw new Error(`No German tuples found — are the cached zips present in ${dataRootPath("oa-cache")}?`)
169
116
  }
170
117
 
171
118
  let emitted = 0
@@ -32,6 +32,7 @@ import { siBareVillageRecipe } from "./si-bare-village.ts"
32
32
  import { streetAffixRecipe } from "./street-affix.ts"
33
33
  import { streetBareRecipe } from "./street-bare.ts"
34
34
  import { streetRecipe } from "./street.ts"
35
+ import { subVenueRecipe } from "./sub-venue.ts"
35
36
  import { unitRecipe } from "./unit.ts"
36
37
 
37
38
  export * from "./scaffold.ts"
@@ -64,6 +65,7 @@ const RECIPES: readonly ShardRecipe[] = [
64
65
  countryBalancedRecipe,
65
66
  boundaryStressRecipe,
66
67
  anchorAbsorptionRecipe,
68
+ subVenueRecipe,
67
69
  ]
68
70
 
69
71
  /**
@@ -30,8 +30,8 @@
30
30
  * External inputs (`--edges-dir`, opts.edgesDir; both already on disk — do not re-download):
31
31
  *
32
32
  * - <edges-dir>/tl_2023_{17031,34027,50023}_edges.shp (unzipped TIGER 2023 EDGES; default
33
- * /tmp/tiger-edges)
34
- * - /tmp/oa-cache/us__il__cook.zip (ZIP→city tails)
33
+ * `$MAILWOMAN_DATA_ROOT/census/tiger2023-edges`, where `mailwoman situs interpolation` puts them)
34
+ * - `$MAILWOMAN_DATA_ROOT/oa-cache/us__il__cook.zip` (ZIP→city tails)
35
35
  */
36
36
 
37
37
  import { spawnSync } from "node:child_process"
@@ -39,12 +39,13 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
39
39
 
40
40
  import type { DuckDBConnection } from "@duckdb/node-api"
41
41
  import type { ComponentTag } from "@mailwoman/core/types"
42
- import { repoRootPath } from "@mailwoman/core/utils"
42
+ import { dataRootPath, repoRootPath } from "@mailwoman/core/utils"
43
+ import { JSONSpliterator } from "spliterator"
43
44
 
44
45
  import { stableSourceID } from "../adapter.ts"
45
46
  import { alignRow } from "../align.ts"
46
47
  import type { CanonicalRow, LabeledRow } from "../types.ts"
47
- import { makeMulberry32, type ShardRecipe } from "./scaffold.ts"
48
+ import { makeMulberry32, readCSVRecords, type ShardRecipe } from "./scaffold.ts"
48
49
 
49
50
  interface County {
50
51
  fips: string
@@ -60,7 +61,13 @@ const TRAIN_COUNTIES: readonly County[] = [
60
61
  const GOLDEN_COUNTIES: readonly County[] = [{ fips: "50023", state: "VT", regime: "rural" }]
61
62
 
62
63
  const EVAL_GOLD_PATH = repoRootPath("data", "eval", "external", "intersection-real.jsonl")
63
- const OA_COOK = { zip: "/tmp/oa-cache/us__il__cook.zip", csv: "us/il/cook.csv" }
64
+ /**
65
+ * Where `mailwoman situs interpolation` unpacks the per-county TIGER EDGES shapefiles — a Census download, so it sits
66
+ * beside the other Census vintages under `census/`. `--edges-dir` overrides it.
67
+ */
68
+ const DEFAULT_EDGES_DIR = dataRootPath("census", "tiger2023-edges")
69
+
70
+ const OA_COOK = { zip: dataRootPath("oa-cache", "us__il__cook.zip"), csv: "us/il/cook.csv" }
64
71
 
65
72
  /**
66
73
  * One real crossing extracted from a county's TIGER EDGES shapefile.
@@ -157,52 +164,22 @@ function weightedPick<T extends { w: number }>(items: readonly T[], random: () =
157
164
  }
158
165
 
159
166
  /**
160
- * Minimal RFC-4180-ish splitter (handles quoted fields) — same as the affix builder.
167
+ * Order-insensitive crossing key, for eval-leakage exclusion + pair dedup.
161
168
  */
162
- function splitCSV(line: string): string[] {
163
- const out: string[] = []
164
- let cur = ""
165
- let inQ = false
166
-
167
- for (let i = 0; i < line.length; i++) {
168
- const c = line[i]
169
-
170
- if (inQ) {
171
- if (c === '"') {
172
- if (line[i + 1] === '"') {
173
- cur += '"'
174
-
175
- i++
176
- } else {
177
- inQ = false
178
- }
179
- } else {
180
- cur += c
181
- }
182
- } else if (c === '"') {
183
- inQ = true
184
- } else if (c === ",") {
185
- out.push(cur)
186
- cur = ""
187
- } else {
188
- cur += c
189
- }
190
- }
191
-
192
- out.push(cur)
193
-
194
- return out
195
- }
169
+ const pairKey = (a: string, b: string): string => [a.toLowerCase(), b.toLowerCase()].toSorted().join("\u001F")
196
170
 
197
171
  /**
198
- * Order-insensitive crossing key, for eval-leakage exclusion + pair dedup.
172
+ * One row of the intersection eval gold — the shape this recipe reads to exclude eval crossings.
199
173
  */
200
- const pairKey = (a: string, b: string): string => [a.toLowerCase(), b.toLowerCase()].toSorted().join("\u001F")
174
+ interface EvalGoldRow {
175
+ node: number | string
176
+ components: { intersection_a: string; intersection_b: string }
177
+ }
201
178
 
202
179
  /**
203
180
  * Load the eval's crossings so neither train nor golden ever sees them.
204
181
  */
205
- function readEvalExclusions(): { nodes: Set<number>; pairs: Set<string> } {
182
+ async function readEvalExclusions(): Promise<{ nodes: Set<number>; pairs: Set<string> }> {
206
183
  const nodes = new Set<number>()
207
184
  const pairs = new Set<string>()
208
185
 
@@ -212,9 +189,7 @@ function readEvalExclusions(): { nodes: Set<number>; pairs: Set<string> } {
212
189
  return { nodes, pairs }
213
190
  }
214
191
 
215
- for (const line of readFileSync(EVAL_GOLD_PATH, "utf8").split("\n")) {
216
- if (!line) continue
217
- const row = JSON.parse(line)
192
+ for await (const row of JSONSpliterator.fromAsync<EvalGoldRow>(EVAL_GOLD_PATH)) {
218
193
  nodes.add(Number(row.node))
219
194
  pairs.add(pairKey(row.components.intersection_a, row.components.intersection_b))
220
195
  }
@@ -285,20 +260,12 @@ function buildZipCityMap(): Map<string, string> {
285
260
  return new Map()
286
261
  }
287
262
 
288
- const lines = r.stdout.toString("utf8").split(/\r?\n/)
289
-
290
- if (lines.length < 2) return new Map()
291
- const header = splitCSV(lines[0]!).map((h) => h.trim().toLowerCase())
292
- const iCity = header.indexOf("city")
293
- const iPost = header.indexOf("postcode")
294
263
  const counts = new Map<string, Map<string, number>>()
295
264
 
296
265
  // zip → Map(city → n)
297
- for (let li = 1; li < lines.length; li++) {
298
- if (!lines[li]) continue
299
- const cells = splitCSV(lines[li]!)
300
- const city = (cells[iCity] ?? "").trim()
301
- const zip = (cells[iPost] ?? "").trim()
266
+ for (const row of readCSVRecords(r.stdout)) {
267
+ const city = row.city ?? ""
268
+ const zip = row.postcode ?? ""
302
269
 
303
270
  if (!city || !/^\d{5}$/.test(zip) || BAD_NAME.test(city)) continue
304
271
  let byCity = counts.get(zip)
@@ -486,15 +453,20 @@ export const intersectionRecipe: ShardRecipe = {
486
453
  name: "intersection",
487
454
  description: "Real-pair intersection rows (US): TIGER 2023 EDGES crossings → audited intersection_a/b labels",
488
455
  mode: "generate",
489
- options: [{ flag: "--edges-dir <dir>", description: "Unzipped TIGER 2023 EDGES dir. Default /tmp/tiger-edges" }],
456
+ options: [
457
+ {
458
+ flag: "--edges-dir <dir>",
459
+ description: `Unzipped TIGER 2023 EDGES dir. Default ${DEFAULT_EDGES_DIR}`,
460
+ },
461
+ ],
490
462
  async run(opts, write) {
491
463
  // Legacy build-intersection-shard.mjs seeded `mulberry32(opts.seed)`.
492
464
  const random = makeMulberry32(opts.seed)
493
465
  const count = opts.count ?? 40_000
494
466
  const source = opts.sourceName ?? "synth-intersection"
495
- const edgesDir = opts.edgesDir ?? "/tmp/tiger-edges"
467
+ const edgesDir = opts.edgesDir ?? DEFAULT_EDGES_DIR
496
468
  const counties = opts.golden ? GOLDEN_COUNTIES : TRAIN_COUNTIES
497
- const exclusions = readEvalExclusions()
469
+ const exclusions = await readEvalExclusions()
498
470
 
499
471
  console.error(` eval exclusions: ${exclusions.nodes.size} nodes, ${exclusions.pairs.size} pairs`)
500
472
 
@@ -86,26 +86,26 @@ export interface LocaleCountrySource {
86
86
  }
87
87
 
88
88
  /**
89
- * Per-country OA sources + the source name used in the corpus. DE/FR still point at the legacy `/tmp/oa-cache` zips
90
- * (their historical build inputs — materialize them there to regenerate). ES/NL read the extracted countrywide CSVs and
91
- * IT the cached national zip under `$MAILWOMAN_DATA_ROOT` (#241; the fresh ES extract is OA-conformed, so the old
92
- * raw-CNIG conform map is gone). DE carries a per-part `region` fallback (its REGION column is empty; the
93
- * international-order tail needs it, #327). FR/NL/IT/ES REGION is populated per-row (ES = comunidad autónoma, IT =
94
- * regione, NL = province).
89
+ * Per-country OA sources + the source name used in the corpus. DE/FR read their historical build inputs, the cached
90
+ * zips under `$MAILWOMAN_DATA_ROOT/oa-cache` — materialize them there to regenerate. ES/NL read the extracted
91
+ * countrywide CSVs and IT the cached national zip under `$MAILWOMAN_DATA_ROOT` (#241; the fresh ES extract is
92
+ * OA-conformed, so the old raw-CNIG conform map is gone). DE carries a per-part `region` fallback (its REGION column is
93
+ * empty; the international-order tail needs it, #327). FR/NL/IT/ES REGION is populated per-row (ES = comunidad
94
+ * autónoma, IT = regione, NL = province).
95
95
  */
96
96
  const COUNTRY_SOURCES: Record<string, LocaleCountrySource> = {
97
97
  DE: {
98
98
  source: "synth-german",
99
99
  corpusVersion: "0.4.0",
100
100
  parts: [
101
- { zip: "/tmp/oa-cache/de__berlin.zip", csv: "de/berlin.csv", region: "Berlin" },
102
- { zip: "/tmp/oa-cache/de__sn__statewide.zip", csv: "de/sn/statewide.csv", region: "Sachsen" },
101
+ { zip: dataRootPath("oa-cache", "de__berlin.zip"), csv: "de/berlin.csv", region: "Berlin" },
102
+ { zip: dataRootPath("oa-cache", "de__sn__statewide.zip"), csv: "de/sn/statewide.csv", region: "Sachsen" },
103
103
  ],
104
104
  },
105
105
  FR: {
106
106
  source: "synth-fr",
107
107
  corpusVersion: "0.4.0",
108
- parts: [{ zip: "/tmp/oa-cache/fr__countrywide.zip", csv: "fr/countrywide.csv" }],
108
+ parts: [{ zip: dataRootPath("oa-cache", "fr__countrywide.zip"), csv: "fr/countrywide.csv" }],
109
109
  },
110
110
  NL: {
111
111
  source: "synth-nl",
@@ -144,7 +144,7 @@ const COUNTRY_SOURCES: Record<string, LocaleCountrySource> = {
144
144
  parts: [{ path: dataRootPath("openaddresses", "extracted", "nz", "countrywide.csv"), districtAsLocality: true }],
145
145
  },
146
146
  GB: {
147
- // HM Land Registry Price Paid Data tuples (25.67M rows; see Task 2's ppd ingest). PPD's DISTRICT is the
147
+ // HM Land Registry Price Paid Data tuples (25.67M rows out of the PPD ingest). PPD's DISTRICT is the
148
148
  // postal town (locality) and CITY is the dependent locality — legitimately EMPTY on the majority of rows
149
149
  // (most GB addresses have no dependent locality). `districtAsLocality` maps DISTRICT→locality and, when
150
150
  // present, CITY→dependent_locality; the `readTuples` gate above only drops a row when BOTH are empty, so
@@ -256,9 +256,8 @@ export async function readTuples(part: LocalePart, rng: () => number): Promise<L
256
256
 
257
257
  try {
258
258
  // CSVSpliterator handles OA's quoted fields (embedded commas/newlines) and CRLF row terminators
259
- // (spliterator ≥ 3.2.0); `header: false` yields the header row too, so we build the column index
260
- // from it exactly as the prior hand-rolled split did. Verified byte-identical tuples vs that split
261
- // on 10M+ real OA rows — the only raw-cell difference was a trailing CR on the discarded HASH column.
259
+ // (spliterator ≥ 3.2.0); `header: false` yields the header row too, so the column index is built
260
+ // from that first row rather than from a header option.
262
261
  for await (const cells of CSVSpliterator.fromAsync<string[]>(input, {
263
262
  mode: "array",
264
263
  header: false,
@@ -315,9 +314,8 @@ export async function readTuples(part: LocalePart, rng: () => number): Promise<L
315
314
  // dependent_locality. When DISTRICT is empty (~18% of NZ rows), fall back to CITY → locality with no
316
315
  // sub-locality. GB PPD tuples flip which side is legitimately empty — on the MAJORITY of GB rows CITY
317
316
  // (the dependent_locality) is empty and DISTRICT (the locality) is populated, so the gate below only
318
- // drops a `districtAsLocality` row when BOTH are empty, not when CITY alone is (that used to silently
319
- // drop most of the GB source — see the fixed `readTuples` gate below). See
320
- // {@link LocalePart.districtAsLocality}.
317
+ // drops a `districtAsLocality` row when BOTH are empty, never when CITY alone is — gating on CITY
318
+ // alone silently discards most of the GB source. See {@link LocalePart.districtAsLocality}.
321
319
  let locality: string | null
322
320
  let dependent_locality: string | undefined
323
321
 
@@ -35,7 +35,7 @@
35
35
  * list. Diacritic-KEEPING normalizer, matching the board (see the norm docstring).
36
36
  */
37
37
 
38
- import { readFileSync } from "node:fs"
38
+ import { TextSpliterator } from "spliterator"
39
39
 
40
40
  import { alignAndWrite, makeMulberry32, readTuples, type ShardRecipe, shardSourceID } from "./scaffold.ts"
41
41
 
@@ -100,12 +100,15 @@ export const noFragmentRecipe: ShardRecipe = {
100
100
  )
101
101
  }
102
102
 
103
- const excluded = new Set(
104
- readFileSync(excludePath, "utf8")
105
- .split("\n")
106
- .map((line) => line.trim())
107
- .filter((line) => line && !line.startsWith("#"))
108
- )
103
+ const excluded = new Set<string>()
104
+
105
+ for await (const line of TextSpliterator.fromAsync(excludePath)) {
106
+ const trimmed = line.trim()
107
+
108
+ if (trimmed && !trimmed.startsWith("#")) {
109
+ excluded.add(trimmed)
110
+ }
111
+ }
109
112
 
110
113
  if (!excluded.size) throw new Error(`no-fragment: --exclude-surfaces "${excludePath}" listed no surfaces`)
111
114
 
@@ -16,7 +16,7 @@
16
16
  * 3. pc-first "«pc» «city», «st» «n»" (the 7% floor — the anchor the others converge to)
17
17
  */
18
18
 
19
- import { readFileSync } from "node:fs"
19
+ import { TextSpliterator } from "spliterator"
20
20
 
21
21
  import { alignAndWrite, makeMulberry32, readTuples, type ShardRecipe, shardSourceID } from "./scaffold.ts"
22
22
 
@@ -62,12 +62,15 @@ export const noStreetLedRecipe: ShardRecipe = {
62
62
  )
63
63
  }
64
64
 
65
- const excluded = new Set(
66
- readFileSync(excludePath, "utf8")
67
- .split("\n")
68
- .map((line) => line.trim())
69
- .filter((line) => line && !line.startsWith("#"))
70
- )
65
+ const excluded = new Set<string>()
66
+
67
+ for await (const line of TextSpliterator.fromAsync(excludePath)) {
68
+ const trimmed = line.trim()
69
+
70
+ if (trimmed && !trimmed.startsWith("#")) {
71
+ excluded.add(trimmed)
72
+ }
73
+ }
71
74
 
72
75
  if (!excluded.size) throw new Error(`no-street-led: --exclude-surfaces "${excludePath}" listed no surfaces`)
73
76