@mailwoman/corpus 8.6.0 → 9.1.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 (497) hide show
  1. package/README.md +2 -2
  2. package/data/PROVENANCE.md +214 -0
  3. package/data/sub-venue-lexicon.json +18269 -0
  4. package/out/src/adapters/ban/adapter.d.ts +5 -4
  5. package/out/src/adapters/ban/adapter.d.ts.map +1 -1
  6. package/out/src/adapters/ban/adapter.js +62 -69
  7. package/out/src/adapters/ban/adapter.js.map +1 -1
  8. package/out/src/adapters/ban/street-decompose.d.ts.map +1 -1
  9. package/out/src/adapters/ban/street-decompose.js +18 -25
  10. package/out/src/adapters/ban/street-decompose.js.map +1 -1
  11. package/out/src/adapters/fcc-bdc/adapter.d.ts +1 -7
  12. package/out/src/adapters/fcc-bdc/adapter.d.ts.map +1 -1
  13. package/out/src/adapters/fcc-bdc/adapter.js +4 -26
  14. package/out/src/adapters/fcc-bdc/adapter.js.map +1 -1
  15. package/out/src/adapters/geonames/adapter.d.ts +1 -1
  16. package/out/src/adapters/geonames/adapter.d.ts.map +1 -1
  17. package/out/src/adapters/geonames/adapter.js +72 -78
  18. package/out/src/adapters/geonames/adapter.js.map +1 -1
  19. package/out/src/adapters/geonames-postal/adapter.d.ts +1 -1
  20. package/out/src/adapters/geonames-postal/adapter.d.ts.map +1 -1
  21. package/out/src/adapters/geonames-postal/adapter.js +46 -51
  22. package/out/src/adapters/geonames-postal/adapter.js.map +1 -1
  23. package/out/src/adapters/gnaf/adapter.d.ts +1 -1
  24. package/out/src/adapters/gnaf/adapter.d.ts.map +1 -1
  25. package/out/src/adapters/gnaf/adapter.js +6 -9
  26. package/out/src/adapters/gnaf/adapter.js.map +1 -1
  27. package/out/src/adapters/gnaf/assemble.d.ts.map +1 -1
  28. package/out/src/adapters/gnaf/assemble.js +7 -12
  29. package/out/src/adapters/gnaf/assemble.js.map +1 -1
  30. package/out/src/adapters/index.d.ts +1 -1
  31. package/out/src/adapters/index.d.ts.map +1 -1
  32. package/out/src/adapters/index.js +1 -1
  33. package/out/src/adapters/index.js.map +1 -1
  34. package/out/src/adapters/openaddresses/adapter.d.ts +1 -1
  35. package/out/src/adapters/openaddresses/adapter.d.ts.map +1 -1
  36. package/out/src/adapters/openaddresses/adapter.js +5 -10
  37. package/out/src/adapters/openaddresses/adapter.js.map +1 -1
  38. package/out/src/adapters/overture/adapter.d.ts +1 -1
  39. package/out/src/adapters/overture/adapter.d.ts.map +1 -1
  40. package/out/src/adapters/overture/adapter.js +5 -9
  41. package/out/src/adapters/overture/adapter.js.map +1 -1
  42. package/out/src/adapters/state-hi-schools/adapter.d.ts +1 -1
  43. package/out/src/adapters/state-hi-schools/adapter.d.ts.map +1 -1
  44. package/out/src/adapters/state-hi-schools/adapter.js +57 -75
  45. package/out/src/adapters/state-hi-schools/adapter.js.map +1 -1
  46. package/out/src/adapters/state-ia-contractors/adapter.d.ts +1 -1
  47. package/out/src/adapters/state-ia-contractors/adapter.d.ts.map +1 -1
  48. package/out/src/adapters/state-ia-contractors/adapter.js +60 -78
  49. package/out/src/adapters/state-ia-contractors/adapter.js.map +1 -1
  50. package/out/src/adapters/state-ny-notaries/adapter.d.ts +1 -1
  51. package/out/src/adapters/state-ny-notaries/adapter.d.ts.map +1 -1
  52. package/out/src/adapters/state-ny-notaries/adapter.js +69 -87
  53. package/out/src/adapters/state-ny-notaries/adapter.js.map +1 -1
  54. package/out/src/adapters/state-tx-notaries/adapter.d.ts +1 -1
  55. package/out/src/adapters/state-tx-notaries/adapter.d.ts.map +1 -1
  56. package/out/src/adapters/state-tx-notaries/adapter.js +71 -89
  57. package/out/src/adapters/state-tx-notaries/adapter.js.map +1 -1
  58. package/out/src/adapters/synth-po-box/adapter.d.ts +3 -3
  59. package/out/src/adapters/synth-po-box/adapter.d.ts.map +1 -1
  60. package/out/src/adapters/synth-po-box/adapter.js +10 -18
  61. package/out/src/adapters/synth-po-box/adapter.js.map +1 -1
  62. package/out/src/adapters/tiger/adapter.d.ts +1 -1
  63. package/out/src/adapters/tiger/adapter.d.ts.map +1 -1
  64. package/out/src/adapters/tiger/adapter.js +2 -2
  65. package/out/src/adapters/tiger/adapter.js.map +1 -1
  66. package/out/src/adapters/tiger/street-decompose.d.ts.map +1 -1
  67. package/out/src/adapters/tiger/street-decompose.js +18 -27
  68. package/out/src/adapters/tiger/street-decompose.js.map +1 -1
  69. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +4 -4
  70. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts.map +1 -1
  71. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js +64 -82
  72. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js.map +1 -1
  73. package/out/src/adapters/usgov-imls-pls/adapter.d.ts +1 -1
  74. package/out/src/adapters/usgov-imls-pls/adapter.d.ts.map +1 -1
  75. package/out/src/adapters/usgov-imls-pls/adapter.js +62 -84
  76. package/out/src/adapters/usgov-imls-pls/adapter.js.map +1 -1
  77. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts +1 -1
  78. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts.map +1 -1
  79. package/out/src/adapters/usgov-irs-bmf/adapter.js +59 -66
  80. package/out/src/adapters/usgov-irs-bmf/adapter.js.map +1 -1
  81. package/out/src/adapters/usgov-nad/adapter.d.ts +1 -1
  82. package/out/src/adapters/usgov-nad/adapter.d.ts.map +1 -1
  83. package/out/src/adapters/usgov-nad/adapter.js +6 -9
  84. package/out/src/adapters/usgov-nad/adapter.js.map +1 -1
  85. package/out/src/adapters/usgov-nppes/adapter.d.ts +1 -1
  86. package/out/src/adapters/usgov-nppes/adapter.d.ts.map +1 -1
  87. package/out/src/adapters/usgov-nppes/adapter.js +60 -78
  88. package/out/src/adapters/usgov-nppes/adapter.js.map +1 -1
  89. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts +1 -1
  90. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts.map +1 -1
  91. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js +53 -73
  92. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js.map +1 -1
  93. package/out/src/{adapter.d.ts → adapters/utils/index.d.ts} +43 -2
  94. package/out/src/adapters/utils/index.d.ts.map +1 -0
  95. package/out/src/{adapter.js → adapters/utils/index.js} +47 -2
  96. package/out/src/adapters/utils/index.js.map +1 -0
  97. package/out/src/adapters/wof-admin-jp/adapter.d.ts +1 -1
  98. package/out/src/adapters/wof-admin-jp/adapter.d.ts.map +1 -1
  99. package/out/src/adapters/wof-admin-jp/adapter.js +24 -6
  100. package/out/src/adapters/wof-admin-jp/adapter.js.map +1 -1
  101. package/out/src/adapters/wof-admin-json/adapter.d.ts +2 -2
  102. package/out/src/adapters/wof-admin-json/adapter.d.ts.map +1 -1
  103. package/out/src/adapters/wof-admin-json/adapter.js +2 -2
  104. package/out/src/adapters/wof-admin-json/adapter.js.map +1 -1
  105. package/out/src/adapters/wof-postalcode-json/adapter.d.ts +2 -2
  106. package/out/src/adapters/wof-postalcode-json/adapter.d.ts.map +1 -1
  107. package/out/src/adapters/wof-postalcode-json/adapter.js +2 -2
  108. package/out/src/adapters/wof-postalcode-json/adapter.js.map +1 -1
  109. package/out/src/build.d.ts +3 -4
  110. package/out/src/build.d.ts.map +1 -1
  111. package/out/src/build.js +6 -8
  112. package/out/src/build.js.map +1 -1
  113. package/out/src/index.d.ts +3 -17
  114. package/out/src/index.d.ts.map +1 -1
  115. package/out/src/index.js +3 -17
  116. package/out/src/index.js.map +1 -1
  117. package/out/src/name-prone-us-suffixes.d.ts +12 -0
  118. package/out/src/name-prone-us-suffixes.d.ts.map +1 -0
  119. package/out/src/name-prone-us-suffixes.js +12 -0
  120. package/out/src/name-prone-us-suffixes.js.map +1 -0
  121. package/out/src/parquet-wrapper/reader.d.ts +8 -0
  122. package/out/src/parquet-wrapper/reader.d.ts.map +1 -1
  123. package/out/src/parquet-wrapper/reader.js +16 -0
  124. package/out/src/parquet-wrapper/reader.js.map +1 -1
  125. package/out/src/parquet-wrapper/schema.d.ts +4 -0
  126. package/out/src/parquet-wrapper/schema.d.ts.map +1 -1
  127. package/out/src/parquet-wrapper/schema.js.map +1 -1
  128. package/out/src/runner.d.ts +2 -2
  129. package/out/src/runner.d.ts.map +1 -1
  130. package/out/src/runner.js +1 -1
  131. package/out/src/runner.js.map +1 -1
  132. package/out/src/shard-recipes/anchor-absorption.d.ts.map +1 -1
  133. package/out/src/shard-recipes/anchor-absorption.js +5 -4
  134. package/out/src/shard-recipes/anchor-absorption.js.map +1 -1
  135. package/out/src/shard-recipes/boundary-stress.d.ts.map +1 -1
  136. package/out/src/shard-recipes/boundary-stress.js +2 -2
  137. package/out/src/shard-recipes/boundary-stress.js.map +1 -1
  138. package/out/src/shard-recipes/country-balanced.d.ts.map +1 -1
  139. package/out/src/shard-recipes/country-balanced.js +75 -75
  140. package/out/src/shard-recipes/country-balanced.js.map +1 -1
  141. package/out/src/shard-recipes/fr-admin-split.js +2 -2
  142. package/out/src/shard-recipes/fr-admin-split.js.map +1 -1
  143. package/out/src/shard-recipes/fr-fragment.d.ts.map +1 -1
  144. package/out/src/shard-recipes/fr-fragment.js +8 -5
  145. package/out/src/shard-recipes/fr-fragment.js.map +1 -1
  146. package/out/src/shard-recipes/fr-lieudit.js +2 -2
  147. package/out/src/shard-recipes/fr-lieudit.js.map +1 -1
  148. package/out/src/shard-recipes/fr-order.d.ts.map +1 -1
  149. package/out/src/shard-recipes/fr-order.js +18 -72
  150. package/out/src/shard-recipes/fr-order.js.map +1 -1
  151. package/out/src/shard-recipes/german.d.ts.map +1 -1
  152. package/out/src/shard-recipes/german.js +17 -70
  153. package/out/src/shard-recipes/german.js.map +1 -1
  154. package/out/src/shard-recipes/house-venue.d.ts.map +1 -1
  155. package/out/src/shard-recipes/house-venue.js +1 -1
  156. package/out/src/shard-recipes/house-venue.js.map +1 -1
  157. package/out/src/shard-recipes/index.d.ts.map +1 -1
  158. package/out/src/shard-recipes/index.js +4 -1
  159. package/out/src/shard-recipes/index.js.map +1 -1
  160. package/out/src/shard-recipes/intersection.d.ts +2 -2
  161. package/out/src/shard-recipes/intersection.d.ts.map +1 -1
  162. package/out/src/shard-recipes/intersection.js +28 -72
  163. package/out/src/shard-recipes/intersection.js.map +1 -1
  164. package/out/src/shard-recipes/locale.d.ts +5 -5
  165. package/out/src/shard-recipes/locale.d.ts.map +1 -1
  166. package/out/src/shard-recipes/locale.js +25 -35
  167. package/out/src/shard-recipes/locale.js.map +1 -1
  168. package/out/src/shard-recipes/no-fragment.d.ts.map +1 -1
  169. package/out/src/shard-recipes/no-fragment.js +8 -5
  170. package/out/src/shard-recipes/no-fragment.js.map +1 -1
  171. package/out/src/shard-recipes/no-street-led.d.ts.map +1 -1
  172. package/out/src/shard-recipes/no-street-led.js +8 -5
  173. package/out/src/shard-recipes/no-street-led.js.map +1 -1
  174. package/out/src/shard-recipes/no-street.d.ts.map +1 -1
  175. package/out/src/shard-recipes/no-street.js +1 -1
  176. package/out/src/shard-recipes/no-street.js.map +1 -1
  177. package/out/src/shard-recipes/po-box-cedex.d.ts +3 -3
  178. package/out/src/shard-recipes/po-box-cedex.d.ts.map +1 -1
  179. package/out/src/shard-recipes/po-box-cedex.js +101 -127
  180. package/out/src/shard-recipes/po-box-cedex.js.map +1 -1
  181. package/out/src/shard-recipes/po-box.d.ts.map +1 -1
  182. package/out/src/shard-recipes/po-box.js +1 -1
  183. package/out/src/shard-recipes/po-box.js.map +1 -1
  184. package/out/src/shard-recipes/scaffold.d.ts +58 -9
  185. package/out/src/shard-recipes/scaffold.d.ts.map +1 -1
  186. package/out/src/shard-recipes/scaffold.js +87 -29
  187. package/out/src/shard-recipes/scaffold.js.map +1 -1
  188. package/out/src/shard-recipes/street-affix.d.ts +54 -0
  189. package/out/src/shard-recipes/street-affix.d.ts.map +1 -1
  190. package/out/src/shard-recipes/street-affix.js +253 -111
  191. package/out/src/shard-recipes/street-affix.js.map +1 -1
  192. package/out/src/shard-recipes/street-bare.d.ts.map +1 -1
  193. package/out/src/shard-recipes/street-bare.js +3 -3
  194. package/out/src/shard-recipes/street-bare.js.map +1 -1
  195. package/out/src/shard-recipes/street.d.ts.map +1 -1
  196. package/out/src/shard-recipes/street.js +2 -2
  197. package/out/src/shard-recipes/street.js.map +1 -1
  198. package/out/src/shard-recipes/sub-venue-sources.d.ts +228 -0
  199. package/out/src/shard-recipes/sub-venue-sources.d.ts.map +1 -0
  200. package/out/src/shard-recipes/sub-venue-sources.js +664 -0
  201. package/out/src/shard-recipes/sub-venue-sources.js.map +1 -0
  202. package/out/src/shard-recipes/sub-venue.d.ts +269 -0
  203. package/out/src/shard-recipes/sub-venue.d.ts.map +1 -0
  204. package/out/src/shard-recipes/sub-venue.js +709 -0
  205. package/out/src/shard-recipes/sub-venue.js.map +1 -0
  206. package/out/src/shard-recipes/unit.d.ts +1 -1
  207. package/out/src/shard-recipes/unit.d.ts.map +1 -1
  208. package/out/src/shard-recipes/unit.js +26 -74
  209. package/out/src/shard-recipes/unit.js.map +1 -1
  210. package/out/src/{synthesize-anchor-absorption.d.ts → synthesizers/anchor-absorption.d.ts} +1 -1
  211. package/out/src/synthesizers/anchor-absorption.d.ts.map +1 -0
  212. package/out/src/{synthesize-anchor-absorption.js → synthesizers/anchor-absorption.js} +1 -1
  213. package/out/src/synthesizers/anchor-absorption.js.map +1 -0
  214. package/out/src/{synthesize-boundary-stress.d.ts → synthesizers/boundary-stress.d.ts} +3 -3
  215. package/out/src/synthesizers/boundary-stress.d.ts.map +1 -0
  216. package/out/src/{synthesize-boundary-stress.js → synthesizers/boundary-stress.js} +2 -2
  217. package/out/src/synthesizers/boundary-stress.js.map +1 -0
  218. package/out/src/{synthesize-german.d.ts → synthesizers/german.d.ts} +2 -2
  219. package/out/src/synthesizers/german.d.ts.map +1 -0
  220. package/out/src/{synthesize-german.js → synthesizers/german.js} +2 -2
  221. package/out/src/synthesizers/german.js.map +1 -0
  222. package/out/src/{synthesize-house-venue.d.ts → synthesizers/house-venue.d.ts} +4 -4
  223. package/out/src/synthesizers/house-venue.d.ts.map +1 -0
  224. package/out/src/{synthesize-house-venue.js → synthesizers/house-venue.js} +4 -4
  225. package/out/src/synthesizers/house-venue.js.map +1 -0
  226. package/out/src/{synthesize-intersection.d.ts → synthesizers/intersection.d.ts} +2 -2
  227. package/out/src/synthesizers/intersection.d.ts.map +1 -0
  228. package/out/src/{synthesize-intersection.js → synthesizers/intersection.js} +1 -1
  229. package/out/src/synthesizers/intersection.js.map +1 -0
  230. package/out/src/{synthesize-no-street.d.ts → synthesizers/no-street.d.ts} +3 -3
  231. package/out/src/synthesizers/no-street.d.ts.map +1 -0
  232. package/out/src/{synthesize-no-street.js → synthesizers/no-street.js} +2 -2
  233. package/out/src/synthesizers/no-street.js.map +1 -0
  234. package/out/src/{synthesize-po-box.d.ts → synthesizers/po-box.d.ts} +2 -2
  235. package/out/src/synthesizers/po-box.d.ts.map +1 -0
  236. package/out/src/{synthesize-po-box.js → synthesizers/po-box.js} +1 -1
  237. package/out/src/synthesizers/po-box.js.map +1 -0
  238. package/out/src/{synthesize-street.d.ts → synthesizers/street.d.ts} +2 -2
  239. package/out/src/synthesizers/street.d.ts.map +1 -0
  240. package/out/src/{synthesize-street.js → synthesizers/street.js} +2 -2
  241. package/out/src/synthesizers/street.js.map +1 -0
  242. package/out/src/{synthesize.d.ts → synthesizers/utils.d.ts} +3 -3
  243. package/out/src/synthesizers/utils.d.ts.map +1 -0
  244. package/out/src/{synthesize.js → synthesizers/utils.js} +4 -15
  245. package/out/src/synthesizers/utils.js.map +1 -0
  246. package/out/src/tools/align-shard.d.ts.map +1 -1
  247. package/out/src/tools/align-shard.js +4 -8
  248. package/out/src/tools/align-shard.js.map +1 -1
  249. package/out/src/tools/audit.d.ts.map +1 -1
  250. package/out/src/tools/audit.js +4 -4
  251. package/out/src/tools/audit.js.map +1 -1
  252. package/out/src/tools/corpus-stats.d.ts +2 -2
  253. package/out/src/tools/corpus-stats.d.ts.map +1 -1
  254. package/out/src/tools/corpus-stats.js +18 -31
  255. package/out/src/tools/corpus-stats.js.map +1 -1
  256. package/out/src/tools/fetch/download.d.ts.map +1 -1
  257. package/out/src/tools/fetch/download.js +5 -6
  258. package/out/src/tools/fetch/download.js.map +1 -1
  259. package/out/src/tools/fetch/imls-pls.d.ts.map +1 -1
  260. package/out/src/tools/fetch/imls-pls.js +3 -15
  261. package/out/src/tools/fetch/imls-pls.js.map +1 -1
  262. package/out/src/tools/fetch/index.d.ts +14 -1
  263. package/out/src/tools/fetch/index.d.ts.map +1 -1
  264. package/out/src/tools/fetch/index.js +14 -1
  265. package/out/src/tools/fetch/index.js.map +1 -1
  266. package/out/src/tools/fetch/nad.d.ts +1 -1
  267. package/out/src/tools/fetch/nad.js +1 -1
  268. package/out/src/tools/fetch/nppes.d.ts.map +1 -1
  269. package/out/src/tools/fetch/nppes.js +8 -14
  270. package/out/src/tools/fetch/nppes.js.map +1 -1
  271. package/out/src/tools/fetch/openaddresses.d.ts +1 -1
  272. package/out/src/tools/fetch/openaddresses.js +1 -1
  273. package/out/src/tools/fetch/ourairports.d.ts +45 -0
  274. package/out/src/tools/fetch/ourairports.d.ts.map +1 -0
  275. package/out/src/tools/fetch/ourairports.js +123 -0
  276. package/out/src/tools/fetch/ourairports.js.map +1 -0
  277. package/out/src/tools/fetch/wikidata-subvenue.d.ts +171 -0
  278. package/out/src/tools/fetch/wikidata-subvenue.d.ts.map +1 -0
  279. package/out/src/tools/fetch/wikidata-subvenue.js +275 -0
  280. package/out/src/tools/fetch/wikidata-subvenue.js.map +1 -0
  281. package/out/src/tools/golden-expand.d.ts.map +1 -1
  282. package/out/src/tools/golden-expand.js +18 -16
  283. package/out/src/tools/golden-expand.js.map +1 -1
  284. package/out/src/tools/golden-promote.d.ts.map +1 -1
  285. package/out/src/tools/golden-promote.js +12 -6
  286. package/out/src/tools/golden-promote.js.map +1 -1
  287. package/out/src/tools/golden-relabel-street.d.ts +196 -0
  288. package/out/src/tools/golden-relabel-street.d.ts.map +1 -0
  289. package/out/src/tools/golden-relabel-street.js +440 -0
  290. package/out/src/tools/golden-relabel-street.js.map +1 -0
  291. package/out/src/tools/index.d.ts +4 -0
  292. package/out/src/tools/index.d.ts.map +1 -1
  293. package/out/src/tools/index.js +4 -0
  294. package/out/src/tools/index.js.map +1 -1
  295. package/out/src/tools/jsonl-to-parquet.d.ts.map +1 -1
  296. package/out/src/tools/jsonl-to-parquet.js +3 -2
  297. package/out/src/tools/jsonl-to-parquet.js.map +1 -1
  298. package/out/src/tools/lint-shard-vocab.d.ts.map +1 -1
  299. package/out/src/tools/lint-shard-vocab.js +3 -2
  300. package/out/src/tools/lint-shard-vocab.js.map +1 -1
  301. package/out/src/tools/lint-shard.d.ts +3 -3
  302. package/out/src/tools/lint-shard.d.ts.map +1 -1
  303. package/out/src/tools/lint-shard.js +23 -32
  304. package/out/src/tools/lint-shard.js.map +1 -1
  305. package/out/src/tools/overlay-manifest.d.ts.map +1 -1
  306. package/out/src/tools/overlay-manifest.js +2 -1
  307. package/out/src/tools/overlay-manifest.js.map +1 -1
  308. package/out/src/tools/overture-subvenue.d.ts +112 -0
  309. package/out/src/tools/overture-subvenue.d.ts.map +1 -0
  310. package/out/src/tools/overture-subvenue.js +144 -0
  311. package/out/src/tools/overture-subvenue.js.map +1 -0
  312. package/out/src/tools/shard-kryptonite.d.ts +1 -1
  313. package/out/src/tools/shard-kryptonite.d.ts.map +1 -1
  314. package/out/src/tools/shard-kryptonite.js +6 -6
  315. package/out/src/tools/shard-kryptonite.js.map +1 -1
  316. package/out/src/tools/shard-translit.d.ts +6 -3
  317. package/out/src/tools/shard-translit.d.ts.map +1 -1
  318. package/out/src/tools/shard-translit.js +9 -8
  319. package/out/src/tools/shard-translit.js.map +1 -1
  320. package/out/src/tools/sub-venue-lexicon.d.ts +507 -0
  321. package/out/src/tools/sub-venue-lexicon.d.ts.map +1 -0
  322. package/out/src/tools/sub-venue-lexicon.js +817 -0
  323. package/out/src/tools/sub-venue-lexicon.js.map +1 -0
  324. package/out/src/tools/sub-venue-promotions.d.ts +94 -0
  325. package/out/src/tools/sub-venue-promotions.d.ts.map +1 -0
  326. package/out/src/tools/sub-venue-promotions.js +266 -0
  327. package/out/src/tools/sub-venue-promotions.js.map +1 -0
  328. package/out/src/{align.d.ts → utils/align.d.ts} +1 -1
  329. package/out/src/utils/align.d.ts.map +1 -0
  330. package/out/src/utils/align.js.map +1 -0
  331. package/out/src/{golden.d.ts → utils/golden.d.ts} +5 -0
  332. package/out/src/utils/golden.d.ts.map +1 -0
  333. package/out/src/{golden.js → utils/golden.js} +24 -10
  334. package/out/src/utils/golden.js.map +1 -0
  335. package/out/src/utils/index.d.ts +14 -0
  336. package/out/src/utils/index.d.ts.map +1 -0
  337. package/out/src/utils/index.js +14 -0
  338. package/out/src/utils/index.js.map +1 -0
  339. package/out/src/utils/license.d.ts.map +1 -0
  340. package/out/src/utils/license.js.map +1 -0
  341. package/out/src/{parquet.d.ts → utils/parquet.d.ts} +2 -2
  342. package/out/src/utils/parquet.d.ts.map +1 -0
  343. package/out/src/{parquet.js → utils/parquet.js} +3 -15
  344. package/out/src/utils/parquet.js.map +1 -0
  345. package/out/src/{split.d.ts → utils/split.d.ts} +8 -1
  346. package/out/src/utils/split.d.ts.map +1 -0
  347. package/out/src/{split.js → utils/split.js} +7 -0
  348. package/out/src/utils/split.js.map +1 -0
  349. package/out/src/utils/tokenize.d.ts.map +1 -0
  350. package/out/src/utils/tokenize.js.map +1 -0
  351. package/out/src/{wof-json.d.ts → utils/wof-json.d.ts} +2 -2
  352. package/out/src/utils/wof-json.d.ts.map +1 -0
  353. package/out/src/{wof-json.js → utils/wof-json.js} +5 -8
  354. package/out/src/utils/wof-json.js.map +1 -0
  355. package/package.json +66 -20
  356. package/src/adapters/ban/adapter.ts +58 -68
  357. package/src/adapters/ban/street-decompose.ts +17 -25
  358. package/src/adapters/fcc-bdc/adapter.ts +5 -36
  359. package/src/adapters/geonames/adapter.ts +67 -76
  360. package/src/adapters/geonames-postal/adapter.ts +45 -54
  361. package/src/adapters/gnaf/adapter.ts +7 -11
  362. package/src/adapters/gnaf/assemble.ts +7 -11
  363. package/src/adapters/index.ts +3 -2
  364. package/src/adapters/openaddresses/adapter.ts +6 -12
  365. package/src/adapters/overture/adapter.ts +6 -10
  366. package/src/adapters/state-hi-schools/adapter.ts +53 -77
  367. package/src/adapters/state-ia-contractors/adapter.ts +55 -80
  368. package/src/adapters/state-ny-notaries/adapter.ts +64 -89
  369. package/src/adapters/state-tx-notaries/adapter.ts +63 -88
  370. package/src/adapters/synth-po-box/adapter.ts +12 -23
  371. package/src/adapters/tiger/adapter.ts +4 -3
  372. package/src/adapters/tiger/street-decompose.ts +21 -31
  373. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +64 -88
  374. package/src/adapters/usgov-imls-pls/adapter.ts +57 -86
  375. package/src/adapters/usgov-irs-bmf/adapter.ts +55 -66
  376. package/src/adapters/usgov-nad/adapter.ts +7 -11
  377. package/src/adapters/usgov-nppes/adapter.ts +54 -79
  378. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +48 -75
  379. package/src/{adapter.ts → adapters/utils/index.ts} +59 -3
  380. package/src/adapters/wof-admin-jp/adapter.ts +35 -9
  381. package/src/adapters/wof-admin-json/adapter.ts +3 -4
  382. package/src/adapters/wof-postalcode-json/adapter.ts +3 -4
  383. package/src/build.ts +12 -11
  384. package/src/index.ts +3 -17
  385. package/src/name-prone-us-suffixes.ts +12 -0
  386. package/src/parquet-wrapper/reader.ts +19 -0
  387. package/src/parquet-wrapper/schema.ts +4 -0
  388. package/src/runner.ts +7 -2
  389. package/src/shard-recipes/anchor-absorption.ts +5 -4
  390. package/src/shard-recipes/boundary-stress.ts +6 -2
  391. package/src/shard-recipes/country-balanced.ts +77 -91
  392. package/src/shard-recipes/fr-admin-split.ts +3 -3
  393. package/src/shard-recipes/fr-fragment.ts +10 -7
  394. package/src/shard-recipes/fr-lieudit.ts +3 -3
  395. package/src/shard-recipes/fr-order.ts +18 -83
  396. package/src/shard-recipes/german.ts +17 -80
  397. package/src/shard-recipes/house-venue.ts +2 -1
  398. package/src/shard-recipes/index.ts +4 -1
  399. package/src/shard-recipes/intersection.ts +37 -74
  400. package/src/shard-recipes/locale.ts +31 -39
  401. package/src/shard-recipes/no-fragment.ts +10 -7
  402. package/src/shard-recipes/no-street-led.ts +10 -7
  403. package/src/shard-recipes/no-street.ts +2 -1
  404. package/src/shard-recipes/po-box-cedex.ts +127 -161
  405. package/src/shard-recipes/po-box.ts +6 -1
  406. package/src/shard-recipes/scaffold.ts +125 -27
  407. package/src/shard-recipes/street-affix.ts +335 -134
  408. package/src/shard-recipes/street-bare.ts +4 -3
  409. package/src/shard-recipes/street.ts +3 -2
  410. package/src/shard-recipes/sub-venue-sources.ts +895 -0
  411. package/src/shard-recipes/sub-venue.ts +982 -0
  412. package/src/shard-recipes/unit.ts +27 -85
  413. package/src/{synthesize-boundary-stress.ts → synthesizers/boundary-stress.ts} +2 -2
  414. package/src/{synthesize-german.ts → synthesizers/german.ts} +2 -2
  415. package/src/{synthesize-house-venue.ts → synthesizers/house-venue.ts} +4 -4
  416. package/src/{synthesize-intersection.ts → synthesizers/intersection.ts} +1 -1
  417. package/src/{synthesize-no-street.ts → synthesizers/no-street.ts} +2 -2
  418. package/src/{synthesize-po-box.ts → synthesizers/po-box.ts} +1 -1
  419. package/src/{synthesize-street.ts → synthesizers/street.ts} +2 -2
  420. package/src/{synthesize.ts → synthesizers/utils.ts} +4 -18
  421. package/src/tools/align-shard.ts +4 -7
  422. package/src/tools/audit.ts +6 -5
  423. package/src/tools/corpus-stats.ts +25 -33
  424. package/src/tools/fetch/download.ts +7 -5
  425. package/src/tools/fetch/imls-pls.ts +4 -18
  426. package/src/tools/fetch/index.ts +14 -1
  427. package/src/tools/fetch/nad.ts +1 -1
  428. package/src/tools/fetch/nppes.ts +9 -16
  429. package/src/tools/fetch/openaddresses.ts +1 -1
  430. package/src/tools/fetch/ourairports.ts +166 -0
  431. package/src/tools/fetch/wikidata-subvenue.ts +386 -0
  432. package/src/tools/golden-expand.ts +18 -13
  433. package/src/tools/golden-promote.ts +15 -6
  434. package/src/tools/golden-relabel-street.ts +685 -0
  435. package/src/tools/index.ts +4 -0
  436. package/src/tools/jsonl-to-parquet.ts +3 -2
  437. package/src/tools/lint-shard-vocab.ts +3 -2
  438. package/src/tools/lint-shard.ts +33 -36
  439. package/src/tools/overlay-manifest.ts +2 -1
  440. package/src/tools/overture-subvenue.ts +215 -0
  441. package/src/tools/shard-kryptonite.ts +8 -9
  442. package/src/tools/shard-translit.ts +22 -11
  443. package/src/tools/sub-venue-lexicon.ts +1250 -0
  444. package/src/tools/sub-venue-promotions.ts +330 -0
  445. package/src/{align.ts → utils/align.ts} +1 -1
  446. package/src/{golden.ts → utils/golden.ts} +25 -11
  447. package/src/utils/index.ts +14 -0
  448. package/src/{parquet.ts → utils/parquet.ts} +5 -19
  449. package/src/{split.ts → utils/split.ts} +8 -2
  450. package/src/{wof-json.ts → utils/wof-json.ts} +5 -8
  451. package/out/src/adapter.d.ts.map +0 -1
  452. package/out/src/adapter.js.map +0 -1
  453. package/out/src/align.d.ts.map +0 -1
  454. package/out/src/align.js.map +0 -1
  455. package/out/src/format.d.ts +0 -14
  456. package/out/src/format.d.ts.map +0 -1
  457. package/out/src/format.js +0 -14
  458. package/out/src/format.js.map +0 -1
  459. package/out/src/golden.d.ts.map +0 -1
  460. package/out/src/golden.js.map +0 -1
  461. package/out/src/license.d.ts.map +0 -1
  462. package/out/src/license.js.map +0 -1
  463. package/out/src/parquet.d.ts.map +0 -1
  464. package/out/src/parquet.js.map +0 -1
  465. package/out/src/split.d.ts.map +0 -1
  466. package/out/src/split.js.map +0 -1
  467. package/out/src/synthesize-anchor-absorption.d.ts.map +0 -1
  468. package/out/src/synthesize-anchor-absorption.js.map +0 -1
  469. package/out/src/synthesize-boundary-stress.d.ts.map +0 -1
  470. package/out/src/synthesize-boundary-stress.js.map +0 -1
  471. package/out/src/synthesize-german.d.ts.map +0 -1
  472. package/out/src/synthesize-german.js.map +0 -1
  473. package/out/src/synthesize-house-venue.d.ts.map +0 -1
  474. package/out/src/synthesize-house-venue.js.map +0 -1
  475. package/out/src/synthesize-intersection.d.ts.map +0 -1
  476. package/out/src/synthesize-intersection.js.map +0 -1
  477. package/out/src/synthesize-no-street.d.ts.map +0 -1
  478. package/out/src/synthesize-no-street.js.map +0 -1
  479. package/out/src/synthesize-po-box.d.ts.map +0 -1
  480. package/out/src/synthesize-po-box.js.map +0 -1
  481. package/out/src/synthesize-street.d.ts.map +0 -1
  482. package/out/src/synthesize-street.js.map +0 -1
  483. package/out/src/synthesize.d.ts.map +0 -1
  484. package/out/src/synthesize.js.map +0 -1
  485. package/out/src/tokenize.d.ts.map +0 -1
  486. package/out/src/tokenize.js.map +0 -1
  487. package/out/src/wof-json.d.ts.map +0 -1
  488. package/out/src/wof-json.js.map +0 -1
  489. package/src/format.ts +0 -14
  490. /package/out/src/{align.js → utils/align.js} +0 -0
  491. /package/out/src/{license.d.ts → utils/license.d.ts} +0 -0
  492. /package/out/src/{license.js → utils/license.js} +0 -0
  493. /package/out/src/{tokenize.d.ts → utils/tokenize.d.ts} +0 -0
  494. /package/out/src/{tokenize.js → utils/tokenize.js} +0 -0
  495. /package/src/{synthesize-anchor-absorption.ts → synthesizers/anchor-absorption.ts} +0 -0
  496. /package/src/{license.ts → utils/license.ts} +0 -0
  497. /package/src/{tokenize.ts → utils/tokenize.ts} +0 -0
@@ -24,19 +24,16 @@
24
24
  /* oxlint-disable sister-software/prefer-region-over-marks -- these markers label steps inside one
25
25
  procedure, not sections of declarations. A region there folds nothing a reader wants folded. */
26
26
 
27
- import { execFile } from "node:child_process"
28
27
  import { existsSync, mkdirSync, statSync } from "node:fs"
29
28
  import { rm } from "node:fs/promises"
30
- import { join } from "node:path"
31
- import { promisify } from "node:util"
29
+ import { basename, join } from "node:path"
32
30
 
31
+ import { extractZipEntry, listZipEntries } from "@mailwoman/core/fs/zip"
33
32
  import { sha256File } from "@mailwoman/core/utils"
34
33
 
35
34
  import type { BaseFetchOptions, FetchSummary } from "./download.ts"
36
35
  import { downloadToFile, readManifest, writeManifest } from "./download.ts"
37
36
 
38
- const execFileAsync = promisify(execFile)
39
-
40
37
  const INDEX_URL = "https://download.cms.gov/nppes/NPI_Files.html"
41
38
  const BASE_URL = "https://download.cms.gov/nppes"
42
39
  const SLUG = "usgov-nppes"
@@ -74,18 +71,13 @@ async function discoverLatestZip(): Promise<string | undefined> {
74
71
  }
75
72
 
76
73
  /**
77
- * Extract the main registry CSV name (npidata_pfile_*.csv) from a ZIP's `unzip -l` listing.
74
+ * The main registry CSV (npidata_pfile_*.csv), which the archive also carries alongside a header file and a per-month
75
+ * change file.
78
76
  */
79
77
  async function findNpidataCSV(zipPath: string): Promise<string | undefined> {
80
- const listing = await execFileAsync("unzip", ["-l", zipPath])
81
-
82
- for (const line of listing.stdout.split("\n")) {
83
- const match = /npidata_pfile\S+\.csv/i.exec(line)
84
-
85
- if (match?.[0]) return match[0]
86
- }
78
+ const entries = await listZipEntries(zipPath)
87
79
 
88
- return undefined
80
+ return entries.find((entry) => /npidata_pfile\S+\.csv/i.test(entry.name))?.name
89
81
  }
90
82
 
91
83
  export async function fetchNPPES(options: FetchNPPESOptions, report?: (line: string) => void): Promise<FetchSummary> {
@@ -148,9 +140,10 @@ export async function fetchNPPES(options: FetchNPPESOptions, report?: (line: str
148
140
  }
149
141
 
150
142
  report?.(` Extracting: ${csvName}`)
151
- await execFileAsync("unzip", ["-o", "-j", zipDest, csvName, "-d", destDir])
152
143
 
153
- const csvDest = join(destDir, csvName)
144
+ const csvDest = join(destDir, basename(csvName))
145
+
146
+ await extractZipEntry(zipDest, csvName, csvDest)
154
147
  const csvSize = statSync(csvDest).size
155
148
  const csvSha = await sha256File(csvDest)
156
149
  report?.(` CSV size: ${(csvSize / 1024 / 1024).toFixed(1)} MB`)
@@ -44,7 +44,7 @@
44
44
  * ```sh
45
45
  * # With token (preferred). Default country: ca. Supports any OA country code (us-west, fr, …)
46
46
  * OA_BATCH_TOKEN=<token> mailwoman corpus fetch openaddresses --country ca \
47
- * --out-root /mnt/playpen/mailwoman-data/corpus/sources
47
+ * --out-root $MAILWOMAN_DATA_ROOT/corpus/sources
48
48
  *
49
49
  * # Without token (will detect + print instructions, then report the failure):
50
50
  * mailwoman corpus fetch openaddresses --country ca
@@ -0,0 +1,166 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Fetch the OurAirports CSV dumps — the VENUE side of the sub-venue corpus arc (#35).
7
+ *
8
+ * Source : https://davidmegginson.github.io/ourairports-data/ (the project's own GitHub Pages
9
+ * mirror of the nightly export; `ourairports.com/data/` redirects here).
10
+ * License: PUBLIC DOMAIN. OurAirports places its data in the public domain and asks only for a
11
+ * courtesy credit — no attribution obligation rides on a derived shard, which makes this
12
+ * the one transport source in the arc with no licensing question at all. Tier A.
13
+ *
14
+ * ## What it is good for, and what it is not
15
+ *
16
+ * `airports.csv` is every airport on earth with ICAO/IATA codes, coordinates, `municipality`, and
17
+ * `iso_country` — 12.7 MB, ~83,000 rows as of 2026-08-04. That is the CONTAINING VENUE for a
18
+ * `<sub-venue>, <venue>, <street>, <locality>, <postcode>` corpus line, and it is better at that job
19
+ * than OSM: every row is named, the name is canonical, and `municipality` gives the locality without
20
+ * a spatial join.
21
+ *
22
+ * It carries NO interior structure. There is no terminal, concourse, gate or pier table — the
23
+ * corpus task says as much ("Good for the venue side of each pair, weaker on interior structure")
24
+ * and a row-level read confirms it. Pair it with the OSM `aeroway` extractor
25
+ * (`@mailwoman/osm/sdk`'s `extractOSMSubVenues`), which is where the sub-venue half comes from.
26
+ *
27
+ * ## Why `downloadToFile` and not `APIClient`
28
+ *
29
+ * `AGENTS.md` routes HTTP through `APIClient`, and that rule is about API REQUESTS — small bodies,
30
+ * repeated calls, rate-limited hosts. This is four static file transfers against a GitHub Pages CDN
31
+ * with no rate limit and nothing to pace, run once per refresh. It uses the same `downloadToFile`
32
+ * every other module in this `fetch/` family uses, which is where the retry and timeout live.
33
+ * The Wikidata sibling (`wikidata-subvenue.ts`) IS an API client and is built on `APIClient`
34
+ * accordingly; the split between the two is the one `AGENTS.md` draws.
35
+ *
36
+ * Invoke via `mailwoman corpus fetch ourairports --out-root <path>`.
37
+ */
38
+
39
+ import { mkdirSync } from "node:fs"
40
+ import { join } from "node:path"
41
+
42
+ import { sha256File } from "@mailwoman/core/utils"
43
+
44
+ import type { BaseFetchOptions, FetchSummary } from "./download.ts"
45
+ import { downloadToFile, writeManifest } from "./download.ts"
46
+
47
+ const SLUG = "ourairports"
48
+
49
+ /**
50
+ * The GitHub Pages mirror the project itself publishes. `ourairports.com/data/*.csv` 302s here, so pointing at the
51
+ * mirror directly saves a redirect and is the URL the project's own README gives.
52
+ */
53
+ const BASE_URL = "https://davidmegginson.github.io/ourairports-data"
54
+
55
+ /**
56
+ * The files worth having, and why each one.
57
+ *
58
+ * `airports.csv` is the payload. The other three are small joins that turn its codes into text: `countries.csv` and
59
+ * `regions.csv` expand `iso_country`/`iso_region` into names (a corpus line needs "Germany", not "DE"), and
60
+ * `runways.csv` is the only file carrying per-airport sub-structure of any kind — runway designators, which are NOT
61
+ * sub-venue designators (nobody addresses mail to a runway) but are worth having on disk as the negative class if the
62
+ * shard ever needs one.
63
+ */
64
+ const FILES = ["airports.csv", "countries.csv", "regions.csv", "runways.csv"] as const
65
+
66
+ export type FetchOurAirportsOptions = BaseFetchOptions
67
+
68
+ interface OurAirportsFileEntry {
69
+ filename: string
70
+ source_url: string
71
+ sha256: string
72
+ bytes: number
73
+ /**
74
+ * The upstream `Last-Modified`, when the CDN gave one. This is the DATA's vintage; `downloaded_at` is only when we
75
+ * asked. `corpus/AGENTS.md` has the standing warning that a file's mtime is not its data's vintage — recording the
76
+ * upstream header is how a later refresh decision gets made on the right number.
77
+ */
78
+ last_modified: string | null
79
+ }
80
+
81
+ interface OurAirportsManifest {
82
+ source: string
83
+ base_url: string
84
+ license: string
85
+ downloaded_at: string
86
+ files: OurAirportsFileEntry[]
87
+ }
88
+
89
+ /**
90
+ * Read the upstream `Last-Modified` with a HEAD. Returns `null` on any failure — provenance metadata is nice to have
91
+ * and must never fail a download that otherwise succeeded.
92
+ */
93
+ async function readLastModified(url: string): Promise<string | null> {
94
+ try {
95
+ const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(30_000) })
96
+
97
+ return res.ok ? res.headers.get("last-modified") : null
98
+ } catch {
99
+ return null
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Download the OurAirports CSVs into `<outRoot>/ourairports/`, with a sibling `MANIFEST.json` carrying each file's
105
+ * origin URL, sha256, byte count and upstream `Last-Modified`.
106
+ */
107
+ export async function fetchOurAirports(
108
+ options: FetchOurAirportsOptions,
109
+ report?: (line: string) => void
110
+ ): Promise<FetchSummary> {
111
+ const destDir = join(options.outRoot, SLUG)
112
+ mkdirSync(destDir, { recursive: true })
113
+
114
+ const entries: OurAirportsFileEntry[] = []
115
+ const failedCodes: string[] = []
116
+ let fetched = 0
117
+ let failed = 0
118
+
119
+ for (const filename of FILES) {
120
+ const url = `${BASE_URL}/${filename}`
121
+ const dest = join(destDir, filename)
122
+
123
+ report?.(`=== ${SLUG} / ${filename}`)
124
+
125
+ try {
126
+ const [{ bytes }, lastModified] = await Promise.all([
127
+ downloadToFile({
128
+ url,
129
+ dest,
130
+ timeoutMs: 600_000,
131
+ retries: 2,
132
+ headers: { "Accept-Encoding": "gzip, br" },
133
+ report,
134
+ }),
135
+ readLastModified(url),
136
+ ])
137
+
138
+ entries.push({
139
+ filename,
140
+ source_url: url,
141
+ sha256: await sha256File(dest),
142
+ bytes,
143
+ last_modified: lastModified,
144
+ })
145
+
146
+ fetched++
147
+ } catch (error) {
148
+ report?.(`✗ ${filename}: ${error instanceof Error ? error.message : String(error)}`)
149
+ failedCodes.push(filename)
150
+
151
+ failed++
152
+ }
153
+ }
154
+
155
+ const manifest: OurAirportsManifest = {
156
+ source: "OurAirports",
157
+ base_url: BASE_URL,
158
+ license: "public domain (courtesy credit requested)",
159
+ downloaded_at: new Date().toISOString(),
160
+ files: entries,
161
+ }
162
+
163
+ await writeManifest(join(destDir, "MANIFEST.json"), manifest)
164
+
165
+ return { fetched, skipped: 0, failed, failedCodes }
166
+ }
@@ -0,0 +1,386 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Fetch the multilingual sub-venue designator vocabulary from Wikidata (#35 wave 1).
7
+ *
8
+ * Source : https://query.wikidata.org/sparql (the Wikidata Query Service).
9
+ * License: CC0. Wikidata's data is public-domain dedicated, so nothing rides on a derived shard.
10
+ * Tier A.
11
+ *
12
+ * ## The pull is CLASS labels, not instance names — and that inversion is the whole design
13
+ *
14
+ * The obvious read of "Wikidata for localized designators" is: fetch every airport terminal entity
15
+ * and read its name in each language. That was tried first and it is the WRONG query. Wikidata
16
+ * knows 246 items that are `instance of / subclass of*` airport terminal, carrying 775 labels
17
+ * between them (measured 2026-08-04), and most of those labels are proper names that translate
18
+ * verbatim — `TWA Flight Center` is spelled `TWA Flight Center` in fifteen languages. The
19
+ * instance layer is thin and its localization is mostly a no-op.
20
+ *
21
+ * The DESIGNATOR is the label of the CLASS. `wd:Q849706` ("airport terminal") is labelled `Terminal`
22
+ * in German, `terminal aéroportuaire` in French, `ターミナルビル` in Japanese, `航站楼` in Chinese —
23
+ * and `skos:altLabel` adds the aliases (`Abfertigungsgebäude`, `Flughafenterminal`, `aerostazione`).
24
+ * Eight concept ids yield 877 label+alias rows across 174 languages, which is the vocabulary the
25
+ * corpus task asked for and the instance query does not contain. {@link SUBVENUE_CONCEPTS} is that
26
+ * list of ids; {@link buildDesignatorLabelQuery} is that query.
27
+ *
28
+ * Instance labels are fetched too ({@link buildTerminalInstanceQuery}), for a different job: they
29
+ * are ATTESTED USAGE — evidence of how a designator combines with a modifier or an identifier in
30
+ * running text. 775 rows is small, and it is a validation set, not a vocabulary.
31
+ *
32
+ * ## What a caller must NOT do with the output
33
+ *
34
+ * A class label is a CONCEPT NAME, not a designator as written in an address. Q849706's Spanish
35
+ * label is `terminal aeroportuaria` and its French is `terminal d'aéroport`; nobody writes either on
36
+ * an envelope, they write `Terminal`. Q247739's Spanish is `puerta de embarque` where the addressed
37
+ * form is `Puerta`. So this fetch produces CANDIDATE SURFACES that need a head-noun/curation pass
38
+ * before any of them reaches `neural/venue-structure.ts`'s designator vocabulary — the lexicon
39
+ * builder marks every one `curated: false` and the burden of promotion is on a human. Wiring the raw
40
+ * pull straight into the span proposer would admit multi-word phrases that match nothing and, worse,
41
+ * admit `hall` in a language where it names an ordinary room.
42
+ *
43
+ * ## Why `APIClient` here when the OurAirports sibling uses `downloadToFile`
44
+ *
45
+ * This is the API-request side of `AGENTS.md`'s split: small JSON bodies, several calls per run, and
46
+ * a host that publishes a rate policy and enforces it with 429s. Pacing, bounded `Retry-After`-aware
47
+ * retry, response caching and `ResourceError` mapping all earn their keep, so it extends
48
+ * {@link APIClient}. `ourairports.ts` is four static file transfers off a CDN and correctly does not.
49
+ *
50
+ * WDQS also REQUIRES a descriptive `User-Agent` naming the tool and a contact — an anonymous or
51
+ * library-default agent is blocked outright by the Wikimedia user-agent policy. See
52
+ * {@link WIKIDATA_USER_AGENT}.
53
+ *
54
+ * Invoke via `mailwoman corpus fetch wikidata-subvenue --out-root <path>`.
55
+ */
56
+
57
+ import { mkdirSync } from "node:fs"
58
+ import { writeFile } from "node:fs/promises"
59
+ import { join } from "node:path"
60
+
61
+ import { APIClient, type ClockLike } from "@mailwoman/core/api"
62
+ import { buildDiskStorage } from "@mailwoman/core/api/disk-storage"
63
+ import { sha256File } from "@mailwoman/core/utils"
64
+
65
+ import type { BaseFetchOptions, FetchSummary } from "./download.ts"
66
+ import { writeManifest } from "./download.ts"
67
+
68
+ const SLUG = "wikidata-subvenue"
69
+
70
+ /**
71
+ * The SPARQL endpoint. Public, no credential.
72
+ */
73
+ export const WDQS_ENDPOINT = "https://query.wikidata.org/sparql"
74
+
75
+ /**
76
+ * The `User-Agent` every request carries.
77
+ *
78
+ * NOT decoration. The Wikimedia user-agent policy blocks requests whose agent is absent, generic, or a library default,
79
+ * and WDQS enforces it — an unidentified client gets a 403 that no amount of retrying fixes. The policy asks for a tool
80
+ * name, a URL, and a contact address, all three of which are here.
81
+ */
82
+ export const WIKIDATA_USER_AGENT =
83
+ "mailwoman/1.0 (https://github.com/sister-software/mailwoman; teffen@sister.software) corpus-subvenue-fetch"
84
+
85
+ /**
86
+ * Minimum spacing between dispatches, in milliseconds.
87
+ *
88
+ * WDQS's published limit is expressed as processing-time budget rather than a request rate, and this run issues fewer
89
+ * than a dozen queries total, so the number is chosen for politeness rather than to sit against a ceiling: one query
90
+ * per second is far inside anything WDQS objects to, and at this volume the whole fetch still completes in seconds.
91
+ *
92
+ * Set as `minRequestIntervalMs` rather than `requestsPerMinute` deliberately — `AGENTS.md` records that
93
+ * `requestsPerMinute` is a BUDGET model whose cooldown lets N requests go out back to back, so it does not deliver N
94
+ * per minute and is not the gate that holds a rate. The interval is.
95
+ */
96
+ const WDQS_MIN_REQUEST_INTERVAL_MS = 1000
97
+
98
+ /**
99
+ * Per-attempt socket-inactivity timeout. WDQS's own query timeout is 60 seconds and it answers with a 500 when a query
100
+ * exceeds it, so a client timeout below that would turn a server-side timeout into a client-side one and lose the error
101
+ * body that says which query was too expensive.
102
+ */
103
+ const WDQS_REQUEST_TIMEOUT_MS = 90_000
104
+
105
+ /**
106
+ * Total attempts (including the first) before giving up on a 429/5xx.
107
+ */
108
+ const WDQS_MAX_ATTEMPTS = 3
109
+
110
+ /**
111
+ * How long a cached SPARQL response stays fresh. A week: the class labels this pulls change on the timescale at which
112
+ * someone edits a Wikidata concept's German alias, which is to say rarely, and a re-run inside a working session should
113
+ * not re-ask.
114
+ */
115
+ const WDQS_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000
116
+
117
+ /**
118
+ * One Wikidata concept whose labels are a designator's multilingual surface set.
119
+ *
120
+ * `designatorID` is the mailwoman-side vocabulary term, matching `neural/venue-structure.ts`'s
121
+ * `VENUE_STRUCTURE_DESIGNATORS` wherever the two overlap. `qid` was resolved by `wbsearchentities` and hand-checked
122
+ * against the entity's English description (recorded below) on 2026-08-04 — a QID picked by search alone is how you end
123
+ * up pulling the labels of a Bronx neighbourhood called Concourse.
124
+ *
125
+ * `wing` is ABSENT and that is a finding, not an oversight: Wikidata has no clean concept for "wing of a building".
126
+ * `wbsearchentities` for "wing" returns a surname, two English villages, a rugby position and a drone company. Since
127
+ * `wing` is the single most valuable designator in the arc — `West Wing` is the one modifier case that already parses,
128
+ * and `East Wing` is the one that does not — its localized surfaces have to come from somewhere else. See the wave-1
129
+ * report.
130
+ */
131
+ export interface SubVenueConcept {
132
+ designatorID: string
133
+ qid: string
134
+ /**
135
+ * The entity's English description, recorded so a future reader can tell at a glance whether the QID still names what
136
+ * we think it names.
137
+ */
138
+ gloss: string
139
+ }
140
+
141
+ /**
142
+ * The concept table. Eight ids, each verified against its English description on 2026-08-04.
143
+ */
144
+ export const SUBVENUE_CONCEPTS: readonly SubVenueConcept[] = [
145
+ { designatorID: "terminal", qid: "Q849706", gloss: "airport terminal — part of an airport" },
146
+ { designatorID: "gate", qid: "Q247739", gloss: "gate — airport facility for passenger loading/unloading" },
147
+ { designatorID: "concourse", qid: "Q862212", gloss: "concourse — place where pathways or roads meet" },
148
+ { designatorID: "campus", qid: "Q209465", gloss: "campus — cluster of buildings used by an educational institution" },
149
+ { designatorID: "building", qid: "Q41176", gloss: "building — structure with a roof and walls" },
150
+ { designatorID: "arcade", qid: "Q186637", gloss: "arcade — covered walk enclosed by a line of arches" },
151
+ { designatorID: "hall", qid: "Q240854", gloss: "hall — large enclosed room" },
152
+ { designatorID: "satellite", qid: "Q15990706", gloss: "satellite terminal — detached airport building" },
153
+ ]
154
+
155
+ /**
156
+ * The class whose instances are fetched for attested-usage evidence: airport terminal.
157
+ */
158
+ const TERMINAL_CLASS_QID = "Q849706"
159
+
160
+ /**
161
+ * Build the class-label query: `rdfs:label` and `skos:altLabel` for every concept, in every language, tagged with which
162
+ * of the two it came from so the lexicon can rank a label above an alias.
163
+ *
164
+ * `VALUES` rather than a property path over the whole class tree — the concept list is closed and hand-verified, and a
165
+ * `wdt:P279*` walk from `building` would drag in every structure type on earth.
166
+ */
167
+ export function buildDesignatorLabelQuery(concepts: readonly SubVenueConcept[] = SUBVENUE_CONCEPTS): string {
168
+ const values = concepts.map((c) => `wd:${c.qid}`).join(" ")
169
+
170
+ return `SELECT ?item ?lang ?label ?kind WHERE {
171
+ VALUES ?item { ${values} }
172
+ { ?item rdfs:label ?label . BIND("label" AS ?kind) }
173
+ UNION
174
+ { ?item skos:altLabel ?label . BIND("alt" AS ?kind) }
175
+ BIND(LANG(?label) AS ?lang)
176
+ }`
177
+ }
178
+
179
+ /**
180
+ * Build the instance-label query — every item that is an `instance of` (through any `subclass of` chain) an airport
181
+ * terminal, with all of its labels. Measured at 246 items / 775 labels on 2026-08-04, well inside WDQS's 60-second
182
+ * budget.
183
+ *
184
+ * A caveat worth knowing before trusting a row: Wikidata's P31 on these is not clean. `Q1322696` (Kigali International
185
+ * Airport) is typed as an airport terminal, so the result set mixes AIRPORTS in with terminals. The consumer filters;
186
+ * this module fetches what the query returns.
187
+ */
188
+ export function buildTerminalInstanceQuery(classQID: string = TERMINAL_CLASS_QID): string {
189
+ return `SELECT ?item ?lang ?label WHERE {
190
+ ?item wdt:P31/wdt:P279* wd:${classQID} .
191
+ ?item rdfs:label ?label .
192
+ BIND(LANG(?label) AS ?lang)
193
+ }`
194
+ }
195
+
196
+ /**
197
+ * The SPARQL JSON results shape, narrowed to the two column types these queries produce.
198
+ */
199
+ export interface SPARQLResults {
200
+ results: {
201
+ bindings: Array<Record<string, { type: string; value: string; "xml:lang"?: string }>>
202
+ }
203
+ }
204
+
205
+ /**
206
+ * Whether a decoded body is a SPARQL results envelope. Used as the cache's write validator so an HTML error page served
207
+ * under a 200 is never persisted for the next run to destructure into `undefined`.
208
+ */
209
+ export function isSPARQLResults(value: unknown): value is SPARQLResults {
210
+ return typeof value === "object" && value !== null && Array.isArray((value as SPARQLResults).results?.bindings)
211
+ }
212
+
213
+ export interface CreateWikidataClientOptions {
214
+ /**
215
+ * On-disk response-cache root. Defaults to a `http-cache` directory beside the fetch output.
216
+ */
217
+ cacheDir: string
218
+ /**
219
+ * Time source powering the pacer and the retry backoff. Defaults to the system clock; tests inject a fake so no suite
220
+ * ever sleeps a real second.
221
+ */
222
+ clock?: ClockLike
223
+ /**
224
+ * Axios overrides, merged over this client's defaults. THE TEST SEAM — pass an `adapter` and no live call is made.
225
+ * Overriding `headers` wholesale would drop the required `User-Agent`, so don't.
226
+ */
227
+ axios?: ConstructorParameters<typeof APIClient>[0]["axios"]
228
+ }
229
+
230
+ /**
231
+ * A Wikidata Query Service client: paced, retrying, disk-cached, and correctly identified.
232
+ */
233
+ export class WikidataClient extends APIClient {
234
+ /**
235
+ * Run one SPARQL query and return its results envelope.
236
+ */
237
+ public async query(sparql: string): Promise<SPARQLResults> {
238
+ const url = new URL(WDQS_ENDPOINT)
239
+ url.searchParams.set("query", sparql)
240
+
241
+ const response = await this.fetch<SPARQLResults>({ url: url.toString() })
242
+
243
+ return response.data
244
+ }
245
+ }
246
+
247
+ /**
248
+ * Construct a {@link WikidataClient} with every default resolved.
249
+ */
250
+ export function createWikidataClient(options: CreateWikidataClientOptions): WikidataClient {
251
+ return new WikidataClient({
252
+ displayName: "Wikidata Query Service",
253
+ minRequestIntervalMs: WDQS_MIN_REQUEST_INTERVAL_MS,
254
+ retry: { maxAttempts: WDQS_MAX_ATTEMPTS },
255
+ clock: options.clock,
256
+ caching: {
257
+ storage: buildDiskStorage({
258
+ directory: options.cacheDir,
259
+ // `value.data` is the cached RESPONSE; `value.data.data` is its body. Passing the response here
260
+ // instead of the body is a silent-failure trap — the predicate returns false for every entry and
261
+ // every run re-fetches while logging "rejected by the configured validate() predicate". Caught
262
+ // on the first live run, 2026-08-04.
263
+ validate: (value) => isSPARQLResults(value.data?.data),
264
+ }),
265
+ ttl: WDQS_CACHE_TTL_MS,
266
+ // The TTL is chosen against Wikidata's edit cadence; letting a CDN header override it would
267
+ // silently replace that reasoning with whatever varnish in front of WDQS happens to send.
268
+ interpretHeader: false,
269
+ },
270
+ axios: {
271
+ headers: {
272
+ "User-Agent": WIKIDATA_USER_AGENT,
273
+ Accept: "application/sparql-results+json",
274
+ },
275
+ timeout: WDQS_REQUEST_TIMEOUT_MS,
276
+ responseType: "json",
277
+ // Axios hands back the RAW STRING when a body fails to parse unless this is off. WDQS serves an
278
+ // HTML error page under some failures, and returning that as `SPARQLResults` would surface as an
279
+ // `undefined` destructure far from the cause.
280
+ transitional: { silentJSONParsing: false },
281
+ ...options.axios,
282
+ },
283
+ })
284
+ }
285
+
286
+ export type FetchWikidataSubVenueOptions = BaseFetchOptions
287
+
288
+ interface WikidataFileEntry {
289
+ filename: string
290
+ query: string
291
+ rows: number
292
+ sha256: string
293
+ bytes: number
294
+ }
295
+
296
+ interface WikidataManifest {
297
+ source: string
298
+ endpoint: string
299
+ license: string
300
+ user_agent: string
301
+ downloaded_at: string
302
+ concepts: readonly SubVenueConcept[]
303
+ files: WikidataFileEntry[]
304
+ }
305
+
306
+ /**
307
+ * Write a JSON payload and return its manifest entry.
308
+ */
309
+ async function writePayload(
310
+ destDir: string,
311
+ filename: string,
312
+ query: string,
313
+ results: SPARQLResults
314
+ ): Promise<WikidataFileEntry> {
315
+ const path = join(destDir, filename)
316
+ const body = JSON.stringify(results, null, 2) + "\n"
317
+ await writeFile(path, body)
318
+
319
+ return {
320
+ filename,
321
+ query,
322
+ rows: results.results.bindings.length,
323
+ sha256: await sha256File(path),
324
+ bytes: Buffer.byteLength(body),
325
+ }
326
+ }
327
+
328
+ /**
329
+ * Run both queries and write their raw SPARQL JSON into `<outRoot>/wikidata-subvenue/`, with a `MANIFEST.json` carrying
330
+ * the endpoint, the exact queries, the concept table, row counts and sha256s.
331
+ *
332
+ * The RAW envelope is written rather than a reshaped one on purpose: the lexicon build is a separate, pure step
333
+ * (`sub-venue-lexicon.ts`) and keeping the fetch output byte-faithful to what WDQS served means a lexicon regeneration
334
+ * never needs the network.
335
+ */
336
+ export async function fetchWikidataSubVenue(
337
+ options: FetchWikidataSubVenueOptions,
338
+ report?: (line: string) => void
339
+ ): Promise<FetchSummary> {
340
+ const destDir = join(options.outRoot, SLUG)
341
+ mkdirSync(destDir, { recursive: true })
342
+
343
+ await using client = createWikidataClient({ cacheDir: join(destDir, "http-cache") })
344
+
345
+ const jobs: Array<{ filename: string; query: string }> = [
346
+ { filename: "designator-labels.json", query: buildDesignatorLabelQuery() },
347
+ { filename: "terminal-instance-labels.json", query: buildTerminalInstanceQuery() },
348
+ ]
349
+
350
+ const files: WikidataFileEntry[] = []
351
+ const failedCodes: string[] = []
352
+ let fetched = 0
353
+ let failed = 0
354
+
355
+ for (const job of jobs) {
356
+ report?.(`=== ${SLUG} / ${job.filename}`)
357
+
358
+ try {
359
+ const results = await client.query(job.query)
360
+ const entry = await writePayload(destDir, job.filename, job.query, results)
361
+ report?.(` ${entry.rows} rows, ${entry.bytes} bytes`)
362
+ files.push(entry)
363
+
364
+ fetched++
365
+ } catch (error) {
366
+ report?.(`✗ ${job.filename}: ${error instanceof Error ? error.message : String(error)}`)
367
+ failedCodes.push(job.filename)
368
+
369
+ failed++
370
+ }
371
+ }
372
+
373
+ const manifest: WikidataManifest = {
374
+ source: "Wikidata Query Service",
375
+ endpoint: WDQS_ENDPOINT,
376
+ license: "CC0",
377
+ user_agent: WIKIDATA_USER_AGENT,
378
+ downloaded_at: new Date().toISOString(),
379
+ concepts: SUBVENUE_CONCEPTS,
380
+ files,
381
+ }
382
+
383
+ await writeManifest(join(destDir, "MANIFEST.json"), manifest)
384
+
385
+ return { fetched, skipped: 0, failed, failedCodes }
386
+ }
@@ -51,7 +51,9 @@ import { dirname } from "node:path"
51
51
 
52
52
  import { ParquetReader } from "@dsnp/parquetjs"
53
53
  import { $private } from "@mailwoman/core/env"
54
- import { dataRootPath, writeJSONL } from "@mailwoman/core/utils"
54
+ import { tryParsingJSON } from "@mailwoman/core/objects"
55
+ import { dataRootPath } from "@mailwoman/core/utils"
56
+ import { createNewlineWriter } from "spliterator"
55
57
 
56
58
  // ── Types ─────────────────────────────────────────────────────────────────
57
59
 
@@ -406,21 +408,17 @@ function parseCandidates(text: string): Candidate[] {
406
408
  // Strip markdown fences the model sometimes wraps around JSON
407
409
  const cleaned = text.replaceAll(/^```(?:json)?\n?|\n?```$/g, "").trim()
408
410
 
409
- try {
410
- const parsed = JSON.parse(cleaned) as unknown
411
+ const parsed = tryParsingJSON(cleaned)
411
412
 
412
- if (Array.isArray(parsed)) return parsed as Candidate[]
413
+ if (Array.isArray(parsed)) return parsed as Candidate[]
413
414
 
414
- // Some providers wrap in {"variants": [...]} or {"candidates": [...]}
415
- if (typeof parsed === "object" && parsed !== null) {
416
- for (const key of ["variants", "candidates", "results"]) {
417
- const v = (parsed as Record<string, unknown>)[key]
415
+ // Some providers wrap in {"variants": [...]} or {"candidates": [...]}
416
+ if (typeof parsed === "object" && parsed !== null) {
417
+ for (const key of ["variants", "candidates", "results"]) {
418
+ const v = (parsed as Record<string, unknown>)[key]
418
419
 
419
- if (Array.isArray(v)) return v as Candidate[]
420
- }
420
+ if (Array.isArray(v)) return v as Candidate[]
421
421
  }
422
- } catch {
423
- // fall through
424
422
  }
425
423
 
426
424
  return []
@@ -555,7 +553,14 @@ export async function expandGolden(
555
553
 
556
554
  await Promise.all(workers)
557
555
 
558
- writeJSONL(outputPath, outRows)
556
+ {
557
+ await using out = createNewlineWriter(outputPath)
558
+
559
+ for (const row of outRows) {
560
+ await out.write(JSON.stringify(row))
561
+ }
562
+ }
563
+
559
564
  report?.(`=== summary ===`)
560
565
  report?.(`seeds processed: ${seeds.length}`)
561
566
  report?.(`candidates kept: ${kept}`)