@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
@@ -17,8 +17,12 @@ export * from "./fetch/download.ts"
17
17
  export * from "./fetch/index.ts"
18
18
  export * from "./golden-expand.ts"
19
19
  export * from "./golden-promote.ts"
20
+ export * from "./golden-relabel-street.ts"
20
21
  export * from "./jsonl-to-parquet.ts"
21
22
  export * from "./lint-shard.ts"
22
23
  export * from "./lint-shard-vocab.ts"
23
24
  export * from "./shard-kryptonite.ts"
25
+ export * from "./overture-subvenue.ts"
26
+ export * from "./sub-venue-lexicon.ts"
27
+ export * from "./sub-venue-promotions.ts"
24
28
  export * from "./shard-translit.ts"
@@ -36,6 +36,7 @@ import { unlink } from "node:fs/promises"
36
36
  import { tmpdir } from "node:os"
37
37
  import { join } from "node:path"
38
38
 
39
+ import { parseJSONStrict } from "@mailwoman/core/objects"
39
40
  import { TextSpliterator } from "spliterator"
40
41
 
41
42
  const REQUIRED_COLUMNS = [
@@ -168,7 +169,7 @@ export async function jsonlToParquet(
168
169
  let lineNo = 0
169
170
 
170
171
  // TextSpliterator, not JSONSpliterator: the staging write below streams the RAW line bytes to
171
- // DuckDB verbatim (JSON.parse here only validates), so a re-serialized JSONSpliterator row would
172
+ // DuckDB verbatim (the parse here only validates), so a re-serialized JSONSpliterator row would
172
173
  // defeat the point. CRLF is handled by the existing `rawLine.trim()` (strips a trailing \r),
173
174
  // same as readline's crlfDelay:Infinity did.
174
175
  for await (const rawLine of TextSpliterator.fromAsync(options.input)) {
@@ -176,7 +177,7 @@ export async function jsonlToParquet(
176
177
  const line = rawLine.trim()
177
178
 
178
179
  if (!line) continue
179
- const row = JSON.parse(line) as Record<string, unknown>
180
+ const row = parseJSONStrict<Record<string, unknown>>(line)
180
181
  assertSpanTriple(row, lineNo)
181
182
  // Write the validated line verbatim; DuckDB's `read_json` projects to the explicit `columns`
182
183
  // map below (extra keys dropped, absent keys → NULL — matching the Python `row.get(c)`).
@@ -37,6 +37,7 @@
37
37
  import { readdirSync } from "node:fs"
38
38
  import { join } from "node:path"
39
39
 
40
+ import { tryParsingJSON } from "@mailwoman/core/objects"
40
41
  import { dataRootPath } from "@mailwoman/core/utils"
41
42
 
42
43
  /**
@@ -162,8 +163,8 @@ async function readRows(con: DuckDBConnection, path: string): Promise<CorpusRow[
162
163
  const rows: CorpusRow[] = []
163
164
 
164
165
  for (const r of raw) {
165
- const tokens = JSON.parse(String(r.tokens)) as unknown
166
- const labels = JSON.parse(String(r.labels)) as unknown
166
+ const tokens = tryParsingJSON(String(r.tokens))
167
+ const labels = tryParsingJSON(String(r.labels))
167
168
 
168
169
  if (!Array.isArray(tokens) || !Array.isArray(labels)) continue
169
170
 
@@ -23,7 +23,7 @@
23
23
  * mappings — flagging matches.
24
24
  * 5. **Basic sanity.** Truncated rows (tokens.length !== labels.length), all-O rows >90% of shard.
25
25
  *
26
- * Output: markdown report on stdout, optional JSON sidecar via `outJson`. The command exits 0 if
26
+ * Output: markdown report on stdout, optional JSON sidecar via `outJSON`. The command exits 0 if
27
27
  * no errors, 1 if any errors (warnings don't gate). Per the design, the MANIFEST entry for a
28
28
  * flagged shard should require `lint_acknowledged: true` before training consumes it.
29
29
  *
@@ -35,10 +35,13 @@
35
35
  * [--out-json /tmp/lint-report.json]
36
36
  */
37
37
 
38
- import { execSync } from "node:child_process"
39
38
  import { existsSync, readFileSync, writeFileSync } from "node:fs"
40
39
  import { fileURLToPath } from "node:url"
41
40
 
41
+ import { parseJSONStrict } from "@mailwoman/core/objects"
42
+
43
+ import { ParquetReader } from "../parquet-wrapper/index.ts"
44
+
42
45
  /**
43
46
  * Occurrences of a forbidden label before it is reported — one or two are noise, five is a pattern.
44
47
  */
@@ -100,7 +103,7 @@ export interface LintCorpusShardOptions {
100
103
  /**
101
104
  * Write a JSON sidecar of the flags + summary here.
102
105
  */
103
- outJson?: string
106
+ outJSON?: string
104
107
  }
105
108
 
106
109
  interface CorpusStats {
@@ -123,31 +126,24 @@ interface LintRulesFile {
123
126
  rules: LintRule[]
124
127
  }
125
128
 
126
- interface ShardRow {
129
+ // The reader's `ParquetRecordLike` constraint is `Record<string, unknown>`, and TypeScript gives a type
130
+ // alias an implicit index signature where an interface has none.
131
+ // oxlint-disable-next-line typescript/consistent-type-definitions -- see above
132
+ type ShardRow = {
127
133
  tokens: string[]
128
134
  labels: string[]
129
135
  }
130
136
 
131
- function readShard(shardPath: string): ShardRow[] {
132
- const py = `
133
- import pyarrow.parquet as pq
134
- import json, sys
135
- t = pq.read_table(${JSON.stringify(shardPath)}, columns=['tokens', 'labels'])
136
- tokens_col = t['tokens'].to_pylist()
137
- labels_col = t['labels'].to_pylist()
138
- for i in range(len(tokens_col)):
139
- sys.stdout.write(json.dumps({"tokens": tokens_col[i], "labels": labels_col[i]}) + "\\n")
140
- `
141
-
142
- const buf = execSync(`python3`, { input: py, maxBuffer: 1024 * 1024 * 1024 })
143
- const rows: ShardRow[] = []
144
-
145
- for (const line of buf.toString("utf8").split("\n")) {
146
- if (!line) continue
147
- rows.push(JSON.parse(line))
148
- }
137
+ /**
138
+ * Stream a shard's `tokens`/`labels` columns.
139
+ *
140
+ * Projected rather than read whole: parquet is columnar, so the twelve unused columns are never touched. The rows feed
141
+ * straight into {@link statsFromShard} and are never all resident.
142
+ */
143
+ async function* streamShard(shardPath: string): AsyncIterable<ShardRow> {
144
+ await using reader = await ParquetReader.openFile<ShardRow>(shardPath)
149
145
 
150
- return rows
146
+ yield* reader.project("tokens", "labels")
151
147
  }
152
148
 
153
149
  interface ShardStats {
@@ -158,16 +154,18 @@ interface ShardStats {
158
154
  allORows: number
159
155
  }
160
156
 
161
- function statsFromShard(rows: ShardRow[]): ShardStats {
157
+ async function statsFromShard(rows: AsyncIterable<ShardRow>): Promise<ShardStats> {
162
158
  const out: ShardStats = {
163
- rowCount: rows.length,
159
+ rowCount: 0,
164
160
  tokens: new Map(),
165
161
  bigrams: new Map(),
166
162
  truncatedRows: 0,
167
163
  allORows: 0,
168
164
  }
169
165
 
170
- for (const row of rows) {
166
+ for await (const row of rows) {
167
+ out.rowCount++
168
+
171
169
  if (row.tokens.length !== row.labels.length) {
172
170
  out.truncatedRows++
173
171
 
@@ -474,27 +472,26 @@ function renderReport(
474
472
  /**
475
473
  * Lint a shard against corpus stats + the anti-pattern rules; print the markdown report to stdout.
476
474
  */
477
- export function lintCorpusShard(
475
+ export async function lintCorpusShard(
478
476
  options: LintCorpusShardOptions,
479
477
  report?: (line: string) => void
480
- ): LintCorpusShardSummary {
478
+ ): Promise<LintCorpusShardSummary> {
481
479
  const rulesPath = options.rulesPath ?? defaultRulesPath()
482
480
  report?.(`Reading corpus stats from ${options.statsPath}...`)
483
- const corpus: CorpusStats = JSON.parse(readFileSync(options.statsPath, "utf8"))
481
+ const corpus = parseJSONStrict<CorpusStats>(readFileSync(options.statsPath, "utf8"))
484
482
 
485
483
  report?.(
486
484
  ` ${corpus.row_count} rows from ${corpus.shard_paths.length} shard(s); ${Object.keys(corpus.tokens).length} tokens, ${Object.keys(corpus.bigrams).length} bigrams`
487
485
  )
488
486
 
489
487
  report?.(`Reading shard from ${options.shardPath}...`)
490
- const rows = readShard(options.shardPath)
491
- report?.(` ${rows.length} rows`)
492
488
 
493
- report?.(`Computing shard stats...`)
494
- const shard = statsFromShard(rows)
489
+ const shard = await statsFromShard(streamShard(options.shardPath))
490
+
491
+ report?.(` ${shard.rowCount} rows`)
495
492
 
496
493
  report?.(`Loading rules from ${rulesPath}...`)
497
- const rulesFile: LintRulesFile = JSON.parse(readFileSync(rulesPath, "utf8"))
494
+ const rulesFile = parseJSONStrict<LintRulesFile>(readFileSync(rulesPath, "utf8"))
498
495
 
499
496
  report?.(`Running checks...`)
500
497
 
@@ -514,9 +511,9 @@ export function lintCorpusShard(
514
511
  writeFileSync(options.outMd, rendered)
515
512
  }
516
513
 
517
- if (options.outJson) {
514
+ if (options.outJSON) {
518
515
  writeFileSync(
519
- options.outJson,
516
+ options.outJSON,
520
517
  JSON.stringify(
521
518
  {
522
519
  shard: options.shardPath,
@@ -26,6 +26,7 @@
26
26
  import { readFileSync, statSync, writeFileSync } from "node:fs"
27
27
  import { join } from "node:path"
28
28
 
29
+ import { parseJSONStrict } from "@mailwoman/core/objects"
29
30
  import { sha256Hex } from "@mailwoman/core/utils"
30
31
 
31
32
  interface ShardDescriptor {
@@ -96,7 +97,7 @@ export interface OverlayManifestOptions {
96
97
  }
97
98
 
98
99
  export async function assembleOverlayManifest(args: OverlayManifestOptions): Promise<void> {
99
- const base = JSON.parse(readFileSync(args.base, "utf8")) as BaseManifest
100
+ const base = parseJSONStrict<BaseManifest>(readFileSync(args.base, "utf8"))
100
101
 
101
102
  if (base.shards.some((s) => s.source === args.source)) {
102
103
  console.log(`WARN: base already contains source '${args.source}' — is this the right base?`)
@@ -0,0 +1,215 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Overture Places source for the sub-venue lexicon (#35 wave 2) — read the sub-venue-bearing
7
+ * category slices of `poi.db` (spatial layer #1, `$MAILWOMAN_DATA_ROOT/poi/poi.db`) and yield
8
+ * {@link SubVenueHarvestRow}s the lexicon builder consumes exactly like an OSM extract.
9
+ *
10
+ * ── WHY OVERTURE AT ALL, when the OSM extractor already exists ────────────────────────────────────
11
+ * Wave 1 measured `concourse` at 21 real Overture rows against 4 in the whole Great Britain OSM
12
+ * extract, 3 of which are a street called CONCOURSE WAY. Overture's `airport_terminal` category is
13
+ * curated venue-interior naming; OSM's `aeroway=terminal` is a building footprint that usually
14
+ * carries the AIRPORT's name. The two sources fail differently, so both are read.
15
+ *
16
+ * ── THE CATEGORY SET IS MEASURED, NOT GUESSED ────────────────────────────────────────────────────
17
+ * A full scan of all 13,681,698 rows (2026-08-05, poi.db vintage 2026-05-20.0) counted, per
18
+ * category, how many named rows carry a designator token. The ranking is NOT what a category name
19
+ * predicts — `gas_station` leads the whole table with 12,996 hits, every one of them the token
20
+ * `station` inside "Holiday Station" / "Chevron Station Seward", and `shoe_store` contributes 708
21
+ * hits of `wing` because Red Wing sells boots. {@link OVERTURE_SUBVENUE_CATEGORIES} is the four
22
+ * categories whose hits survived reading the distribution; the rejects are listed below it so nobody
23
+ * re-proposes them.
24
+ *
25
+ * ── poi.db IS FOUR COUNTRIES ─────────────────────────────────────────────────────────────────────
26
+ * Measured the same day: US 11,521,612 / CA 794,418 / FR 721,352 / MX 644,316, and nothing else. The
27
+ * shipped layer is not a world gazetteer, so Overture can attest en-US, en-CA, fr-FR and es-MX
28
+ * surfaces and NOTHING ELSE. Every non-Latin designator the corpus task asks for — `ターミナル`,
29
+ * `Halle`, `Flügel` — has to come from the OSM leg. Do not read a zero count here as evidence of
30
+ * absence in the world; it is evidence of absence in four countries.
31
+ *
32
+ * ── The row shape fits; the PROVENANCE STAMP did not ─────────────────────────────────────────────
33
+ * Wave 1's `OSMSubVenueRow` was written to accept a non-OSM row, and it does: an Overture row is
34
+ * `{ designatorID, name }` with no `ref` and no `localizedNames`. What did NOT fit is
35
+ * `extractAttestedPhrases`, which hardcoded `osm:name` as the surface's `source`. Feeding Overture
36
+ * rows through it unchanged would have labelled every Overture surface as OSM-attested — a
37
+ * provenance lie, and under ODbL a consequential one. Hence the `source`/`region` options on that
38
+ * function and the source-neutral {@link SubVenueHarvestRow} name.
39
+ */
40
+
41
+ import { DatabaseSync } from "node:sqlite"
42
+
43
+ import { DatabaseClient } from "@mailwoman/core/kysley/client"
44
+
45
+ import type { SubVenueHarvestRow } from "./sub-venue-lexicon.ts"
46
+
47
+ /**
48
+ * Overture Places category → the designator its rows attest.
49
+ *
50
+ * Each entry carries the 2026-08-05 full-scan measurement in a comment: `rows` is the category's total, `hits` is how
51
+ * many of its named rows contain any designator token, and the token list is the top of that distribution.
52
+ *
53
+ * REJECTED, with the number that rejected them — do not re-add without a fresh census:
54
+ *
55
+ * - `gas_station` (12,996 hits) — all `station` inside a brand name (Holiday Station, Chevron Station Seward).
56
+ * - `fire_station` (10,377) — same, plus 330 `hall` from fire halls, which are venues rather than sub-venues.
57
+ * - `town_hall` (5,528) — `City Hall` is a whole building, the `venue` tier, not interior structure.
58
+ * - `building_supply_store` (4,030) — `building` inside "Allied Building Products".
59
+ * - `jehovahs_witness_place_of_worship` (2,334) — every hit is "Kingdom Hall of Jehovah's Witnesses".
60
+ * - `shoe_store` (909, of which 708 `wing`) — Red Wing. This one is a CONFOUND BOARD entry, not a source.
61
+ * - `college_university` (3,697) — 2,082 `campus`, but the row names the whole institution; `campus_building` is the
62
+ * interior slice and is kept instead.
63
+ * - `airport` (6,000 rows, 4,302 hits) — 4,071 of them are the token `airport` in the aerodrome's own name. Venue tier,
64
+ * already covered by OurAirports, and it drowns the interior signal.
65
+ * - `transport_interchange` (1 row), `rail_facility_or_service` (81 rows, 2 hits) — too small to matter.
66
+ * - `public_transit_facility_or_service` (2,274 rows, 185 hits) — 161 are `station` naming the station itself.
67
+ */
68
+ export const OVERTURE_SUBVENUE_CATEGORIES: Readonly<Record<string, string>> = {
69
+ // 1,022 rows / 669 hits — terminal 266, concourse 19, gate 13, hall 4. The densest interior naming
70
+ // in the layer and the reason this reader exists.
71
+ airport_terminal: "terminal",
72
+ // 7,366 rows / 3,382 hits — hall 2,134, building 1,019, campus 209. US academic halls and numbered
73
+ // campus buildings: "UAA Cuddy Hall", "UAA Science Building". The row is a building ON a campus, so
74
+ // `campus` is its CONTEXT designator; the phrase found inside the name decides the record it
75
+ // attests, which for these rows is mostly `hall`.
76
+ campus_building: "campus",
77
+ // 443 rows / 301 hits — pier 282, terminal 17.
78
+ pier: "pier",
79
+ // 388 rows / 66 hits — airport 36, terminal 20, concourse 5, gate 3. Small, but the hits are almost
80
+ // all genuine ("Delta Sky Club Concourse B").
81
+ airport_lounge: "terminal",
82
+ }
83
+
84
+ /**
85
+ * The `poi` and `poi_category_codes` columns this reader touches, declared LOCALLY.
86
+ *
87
+ * `@mailwoman/resolver-wof-sqlite` owns the full `POIDatabase` interface, and `@mailwoman/corpus` does not depend on it
88
+ * — the same dependency-direction call `sub-venue-lexicon.ts` makes for `@mailwoman/osm`'s row type. This is a
89
+ * read-only projection of four columns; `overture-subvenue.test.ts` builds a fixture with exactly this DDL, so a column
90
+ * rename upstream fails a test here rather than throwing at runtime against a 3.9 GB database nobody has in CI.
91
+ */
92
+ interface POIReadDatabase {
93
+ poi: {
94
+ category_id: number
95
+ name: string | null
96
+ country: string
97
+ }
98
+ poi_category_codes: {
99
+ id: number
100
+ category: string
101
+ }
102
+ layer_manifest: {
103
+ name: string
104
+ source_vintage: string
105
+ }
106
+ }
107
+
108
+ /**
109
+ * The layer's `source_vintage` — the Overture release the rows came from (`2026-05-20.0`), for the lexicon's
110
+ * `sources[]`.
111
+ *
112
+ * Read off the database rather than passed in, because a vintage a caller types is a vintage that goes stale silently.
113
+ * The layer-contract tables are part of every layer database by construction — see `docs/engineering/reference/
114
+ * layer-contract.mdx` — so there is no version of poi.db where this is absent.
115
+ */
116
+ export async function readOvertureLayerVintage(databasePath: string): Promise<string> {
117
+ using kdb = new DatabaseClient<POIReadDatabase>({
118
+ database: new DatabaseSync(databasePath, { readOnly: true }),
119
+ })
120
+
121
+ const row = await kdb
122
+ .selectFrom("layer_manifest")
123
+ .select("source_vintage")
124
+ .where("name", "=", "poi")
125
+ .executeTakeFirst()
126
+
127
+ return row?.source_vintage ?? ""
128
+ }
129
+
130
+ export interface ReadOvertureSubVenuesOptions {
131
+ /**
132
+ * Path to `poi.db`. Typically `dataRootPath("poi", "poi.db")`.
133
+ */
134
+ databasePath: string
135
+ /**
136
+ * Category → designator map. Defaults to {@link OVERTURE_SUBVENUE_CATEGORIES}.
137
+ */
138
+ categories?: Readonly<Record<string, string>>
139
+ /**
140
+ * Restrict to these ISO 3166-1 alpha-2 countries. Omit for all four the layer carries.
141
+ */
142
+ countries?: readonly string[]
143
+ }
144
+
145
+ /**
146
+ * One Overture row plus the country it came from — {@link SubVenueHarvestRow} widened by the field the layer has and an
147
+ * OSM extract does not (a Geofabrik extract's country is a property of the invocation, so OSM rows carry `country:
148
+ * ""`).
149
+ */
150
+ export interface OvertureSubVenueRow extends SubVenueHarvestRow {
151
+ /**
152
+ * ISO 3166-1 alpha-2, straight off the Overture partition.
153
+ */
154
+ country: string
155
+ /**
156
+ * The Overture category the row was read from, so a surface's provenance survives past the designator.
157
+ */
158
+ category: string
159
+ }
160
+
161
+ /**
162
+ * Read the sub-venue-bearing category slices of `poi.db`.
163
+ *
164
+ * Cold path, already async, no interface constraint — so Kysely, per the repo's inline-SQL rule.
165
+ *
166
+ * `category_id` is the SECOND component of poi.db's clustered `(h3_cell, category_id, …)` primary key, which reads like
167
+ * a full 13.7M-row scan and is not one: SQLite skip-scans the leading `h3_cell` and the whole four-category read
168
+ * returned 9,219 rows in **1.4 s** on the shipped layer (2026-08-05). A JS-side scan of every row to survey the same
169
+ * question took 52 s, which is the number to remember if you are tempted to filter in JS instead.
170
+ */
171
+ export async function readOvertureSubVenues(options: ReadOvertureSubVenuesOptions): Promise<OvertureSubVenueRow[]> {
172
+ const categories = options.categories ?? OVERTURE_SUBVENUE_CATEGORIES
173
+
174
+ using kdb = new DatabaseClient<POIReadDatabase>({
175
+ database: new DatabaseSync(options.databasePath, { readOnly: true }),
176
+ })
177
+
178
+ const codes = await kdb
179
+ .selectFrom("poi_category_codes")
180
+ .select(["id", "category"])
181
+ .where("category", "in", Object.keys(categories))
182
+ .execute()
183
+
184
+ if (!codes.length) return []
185
+
186
+ const designatorByID = new Map<number, string>()
187
+ const categoryByID = new Map<number, string>()
188
+
189
+ for (const code of codes) {
190
+ const designator = categories[code.category]
191
+
192
+ if (!designator) continue
193
+ designatorByID.set(code.id, designator)
194
+ categoryByID.set(code.id, code.category)
195
+ }
196
+
197
+ let query = kdb
198
+ .selectFrom("poi")
199
+ .select(["category_id", "name", "country"])
200
+ .where("category_id", "in", [...designatorByID.keys()])
201
+ .where("name", "is not", null)
202
+
203
+ if (options.countries?.length) {
204
+ query = query.where("country", "in", [...options.countries])
205
+ }
206
+
207
+ const rows = await query.execute()
208
+
209
+ return rows.map((row) => ({
210
+ designatorID: designatorByID.get(row.category_id)!,
211
+ name: row.name,
212
+ country: row.country,
213
+ category: categoryByID.get(row.category_id)!,
214
+ }))
215
+ }
@@ -7,7 +7,7 @@
7
7
  * MANIFEST. corpus-v0.4.0 is a pure adapter-addition revision: it points at every shard from
8
8
  * v0.3.0 plus the new kryptonite shard(s). No v0.3.0 bytes are touched or re-shuffled.
9
9
  *
10
- * See docs/articles/plan/reference/CORPUS_V0_4_0_GENERATION.md for the why; that doc also pins the
10
+ * See docs/engineering/reference/CORPUS_V0_4_0_GENERATION.md for the why; that doc also pins the
11
11
  * DeepSeek model version + prompt versions used to produce the JSONL.
12
12
  *
13
13
  * Invoke via `mailwoman corpus shard kryptonite \
@@ -20,7 +20,8 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
20
20
  import { mkdir } from "node:fs/promises"
21
21
  import { join } from "node:path"
22
22
 
23
- import { iterateJSONL } from "@mailwoman/core/utils"
23
+ import { parseJSONStrict } from "@mailwoman/core/objects"
24
+ import { JSONSpliterator } from "spliterator"
24
25
 
25
26
  import { alignRow } from "../align.ts"
26
27
  import type { ShardManifest } from "../parquet.ts"
@@ -42,7 +43,7 @@ export interface ShardKryptoniteOptions {
42
43
  }
43
44
 
44
45
  async function* canonicalRows(jsonl: string, corpusVersion: string): AsyncIterable<CanonicalRow> {
45
- for await (const raw of iterateJSONL<Record<string, unknown>>(jsonl)) {
46
+ for await (const raw of JSONSpliterator.fromAsync<Record<string, unknown>>(jsonl)) {
46
47
  // Strip sidecar underscore-prefixed fields the generator left behind for debugging.
47
48
  const components = raw["components"] as Record<string, string>
48
49
 
@@ -111,7 +112,7 @@ export async function buildKryptoniteShard(
111
112
  }
112
113
 
113
114
  // Compose the final corpus-v0.4.0 manifest: every shard from base + the new shard(s).
114
- const base = JSON.parse(readFileSync(options.baseManifest, "utf8")) as ShardManifest
115
+ const base = parseJSONStrict<ShardManifest>(readFileSync(options.baseManifest, "utf8"))
115
116
 
116
117
  const combined: ShardManifest = {
117
118
  corpus_version: corpusVersion,
@@ -16,10 +16,10 @@
16
16
  * - Each shard is written to `train/part-translit-<slug>.parquet` (distinct from kryptonite's
17
17
  * `part-0000.parquet`, which v0.4.0's first builder already produced).
18
18
  * - Inherits the path-canonicalization fix flagged in Thread B's postmortem: v0.3.0 shard paths are
19
- * rewritten from `/mnt/playpen/mailwoman-data/...` to `/data/...` in the combined MANIFEST so
19
+ * rewritten from `$MAILWOMAN_DATA_ROOT/...` to `/data/...` in the combined MANIFEST so
20
20
  * all paths share one container-friendly form.
21
21
  *
22
- * See docs/articles/plan/reference/CORPUS_V0_4_0_GENERATION.md for prompts, model, and the
22
+ * See docs/engineering/reference/CORPUS_V0_4_0_GENERATION.md for prompts, model, and the
23
23
  * reproducibility contract.
24
24
  *
25
25
  * Invoke via `mailwoman corpus shard translit \
@@ -32,7 +32,9 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs"
32
32
  import { mkdir, stat } from "node:fs/promises"
33
33
  import { join } from "node:path"
34
34
 
35
- import { iterateJSONL, sha256File } from "@mailwoman/core/utils"
35
+ import { parseJSONStrict } from "@mailwoman/core/objects"
36
+ import { mailwomanDataRoot, sha256File } from "@mailwoman/core/utils"
37
+ import { JSONSpliterator } from "spliterator"
36
38
 
37
39
  import { alignRow } from "../align.ts"
38
40
  import { ParquetWriter } from "../parquet-wrapper/index.ts"
@@ -53,7 +55,10 @@ export interface ShardTranslitOptions {
53
55
  */
54
56
  canonicalPathPrefix?: string
55
57
  /**
56
- * Default `"/mnt/playpen/mailwoman-data/"`.
58
+ * Prefix the base manifest's shard paths currently carry, to be rewritten to
59
+ * {@link ShardTranslitOptions.canonicalPathPrefix}. Defaults to `mailwomanDataRoot()` with a trailing slash — the
60
+ * root that WROTE those paths. Pass it explicitly when translating a manifest generated under a different
61
+ * `$MAILWOMAN_DATA_ROOT` than the one you are running with.
57
62
  */
58
63
  legacyPathPrefix?: string
59
64
  }
@@ -164,7 +169,7 @@ export async function buildTranslitShard(
164
169
  ): Promise<void> {
165
170
  const corpusVersion = options.corpusVersion ?? "0.4.0"
166
171
  const canonicalPathPrefix = options.canonicalPathPrefix ?? "/data/"
167
- const legacyPathPrefix = options.legacyPathPrefix ?? "/mnt/playpen/mailwoman-data/"
172
+ const legacyPathPrefix = options.legacyPathPrefix ?? `${mailwomanDataRoot()}/`
168
173
 
169
174
  if (!existsSync(options.jsonl)) throw new Error(`jsonl not found: ${options.jsonl}`)
170
175
 
@@ -179,7 +184,7 @@ export async function buildTranslitShard(
179
184
  const quarantine: string[] = []
180
185
  let totalIn = 0
181
186
 
182
- for await (const raw of iterateJSONL<Record<string, unknown>>(options.jsonl)) {
187
+ for await (const raw of JSONSpliterator.fromAsync<Record<string, unknown>>(options.jsonl)) {
183
188
  totalIn++
184
189
  const canon = toCanonicalRow(raw, corpusVersion)
185
190
  const result = alignRow(canon)
@@ -222,7 +227,7 @@ export async function buildTranslitShard(
222
227
  // Compose final MANIFEST: rewrite base.shards paths from /mnt/playpen/... → /data/... and append
223
228
  // the new translit shards. Kryptonite shard already lives in the base manifest (it was written
224
229
  // there by Thread B).
225
- const base = JSON.parse(readFileSync(options.baseManifest, "utf8")) as ShardManifest
230
+ const base = parseJSONStrict<ShardManifest>(readFileSync(options.baseManifest, "utf8"))
226
231
 
227
232
  const rewrittenBase = base.shards.map((sh) => ({
228
233
  ...sh,