@mailwoman/corpus 9.1.0 → 9.2.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 (436) hide show
  1. package/README.md +2 -2
  2. package/data/PROVENANCE.md +13 -0
  3. package/data/reviewed-ve-postcode-tuples.json +74 -0
  4. package/out/src/adapters/ban/adapter.d.ts +1 -1
  5. package/out/src/adapters/ban/adapter.d.ts.map +1 -1
  6. package/out/src/adapters/ban/adapter.js +4 -3
  7. package/out/src/adapters/ban/adapter.js.map +1 -1
  8. package/out/src/adapters/fcc-bdc/adapter.d.ts +1 -1
  9. package/out/src/adapters/fcc-bdc/adapter.d.ts.map +1 -1
  10. package/out/src/adapters/fcc-bdc/adapter.js +2 -2
  11. package/out/src/adapters/fcc-bdc/adapter.js.map +1 -1
  12. package/out/src/adapters/geonames/adapter.d.ts +1 -1
  13. package/out/src/adapters/geonames/adapter.d.ts.map +1 -1
  14. package/out/src/adapters/geonames/adapter.js +1 -1
  15. package/out/src/adapters/geonames/adapter.js.map +1 -1
  16. package/out/src/adapters/geonames-postal/adapter.d.ts +1 -1
  17. package/out/src/adapters/geonames-postal/adapter.d.ts.map +1 -1
  18. package/out/src/adapters/geonames-postal/adapter.js +2 -2
  19. package/out/src/adapters/geonames-postal/adapter.js.map +1 -1
  20. package/out/src/adapters/gnaf/adapter.d.ts +1 -1
  21. package/out/src/adapters/gnaf/adapter.d.ts.map +1 -1
  22. package/out/src/adapters/gnaf/adapter.js +1 -1
  23. package/out/src/adapters/gnaf/adapter.js.map +1 -1
  24. package/out/src/adapters/index.d.ts +1 -1
  25. package/out/src/adapters/index.d.ts.map +1 -1
  26. package/out/src/adapters/index.js +1 -1
  27. package/out/src/adapters/index.js.map +1 -1
  28. package/out/src/adapters/openaddresses/adapter.d.ts +1 -1
  29. package/out/src/adapters/openaddresses/adapter.d.ts.map +1 -1
  30. package/out/src/adapters/openaddresses/adapter.js +2 -2
  31. package/out/src/adapters/openaddresses/adapter.js.map +1 -1
  32. package/out/src/adapters/overture/adapter.d.ts +1 -1
  33. package/out/src/adapters/overture/adapter.d.ts.map +1 -1
  34. package/out/src/adapters/overture/adapter.js +1 -1
  35. package/out/src/adapters/overture/adapter.js.map +1 -1
  36. package/out/src/adapters/state-hi-schools/adapter.d.ts +1 -1
  37. package/out/src/adapters/state-hi-schools/adapter.d.ts.map +1 -1
  38. package/out/src/adapters/state-hi-schools/adapter.js +6 -5
  39. package/out/src/adapters/state-hi-schools/adapter.js.map +1 -1
  40. package/out/src/adapters/state-ia-contractors/adapter.d.ts +1 -1
  41. package/out/src/adapters/state-ia-contractors/adapter.d.ts.map +1 -1
  42. package/out/src/adapters/state-ia-contractors/adapter.js +11 -6
  43. package/out/src/adapters/state-ia-contractors/adapter.js.map +1 -1
  44. package/out/src/adapters/state-ny-notaries/adapter.d.ts +1 -1
  45. package/out/src/adapters/state-ny-notaries/adapter.d.ts.map +1 -1
  46. package/out/src/adapters/state-ny-notaries/adapter.js +11 -17
  47. package/out/src/adapters/state-ny-notaries/adapter.js.map +1 -1
  48. package/out/src/adapters/state-tx-notaries/adapter.d.ts +1 -1
  49. package/out/src/adapters/state-tx-notaries/adapter.d.ts.map +1 -1
  50. package/out/src/adapters/state-tx-notaries/adapter.js +11 -6
  51. package/out/src/adapters/state-tx-notaries/adapter.js.map +1 -1
  52. package/out/src/adapters/synth-po-box/adapter.d.ts +2 -2
  53. package/out/src/adapters/synth-po-box/adapter.d.ts.map +1 -1
  54. package/out/src/adapters/synth-po-box/adapter.js +2 -2
  55. package/out/src/adapters/synth-po-box/adapter.js.map +1 -1
  56. package/out/src/adapters/tiger/adapter.d.ts +1 -1
  57. package/out/src/adapters/tiger/adapter.d.ts.map +1 -1
  58. package/out/src/adapters/tiger/adapter.js +1 -1
  59. package/out/src/adapters/tiger/adapter.js.map +1 -1
  60. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +1 -1
  61. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts.map +1 -1
  62. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js +6 -5
  63. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js.map +1 -1
  64. package/out/src/adapters/usgov-imls-pls/adapter.d.ts +1 -1
  65. package/out/src/adapters/usgov-imls-pls/adapter.d.ts.map +1 -1
  66. package/out/src/adapters/usgov-imls-pls/adapter.js +10 -6
  67. package/out/src/adapters/usgov-imls-pls/adapter.js.map +1 -1
  68. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts +1 -1
  69. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts.map +1 -1
  70. package/out/src/adapters/usgov-irs-bmf/adapter.js +5 -4
  71. package/out/src/adapters/usgov-irs-bmf/adapter.js.map +1 -1
  72. package/out/src/adapters/usgov-nad/adapter.d.ts +1 -1
  73. package/out/src/adapters/usgov-nad/adapter.d.ts.map +1 -1
  74. package/out/src/adapters/usgov-nad/adapter.js +7 -7
  75. package/out/src/adapters/usgov-nad/adapter.js.map +1 -1
  76. package/out/src/adapters/usgov-nppes/adapter.d.ts +1 -1
  77. package/out/src/adapters/usgov-nppes/adapter.d.ts.map +1 -1
  78. package/out/src/adapters/usgov-nppes/adapter.js +9 -9
  79. package/out/src/adapters/usgov-nppes/adapter.js.map +1 -1
  80. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts +1 -1
  81. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts.map +1 -1
  82. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js +6 -5
  83. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js.map +1 -1
  84. package/out/src/adapters/utils/index.d.ts +7 -1
  85. package/out/src/adapters/utils/index.d.ts.map +1 -1
  86. package/out/src/adapters/utils/index.js +10 -2
  87. package/out/src/adapters/utils/index.js.map +1 -1
  88. package/out/src/adapters/wof-admin-jp/adapter.d.ts +1 -1
  89. package/out/src/adapters/wof-admin-jp/adapter.d.ts.map +1 -1
  90. package/out/src/adapters/wof-admin-jp/adapter.js +5 -5
  91. package/out/src/adapters/wof-admin-jp/adapter.js.map +1 -1
  92. package/out/src/adapters/wof-admin-json/adapter.d.ts +4 -15
  93. package/out/src/adapters/wof-admin-json/adapter.d.ts.map +1 -1
  94. package/out/src/adapters/wof-admin-json/adapter.js +11 -35
  95. package/out/src/adapters/wof-admin-json/adapter.js.map +1 -1
  96. package/out/src/adapters/wof-json-rows.d.ts +32 -0
  97. package/out/src/adapters/wof-json-rows.d.ts.map +1 -0
  98. package/out/src/adapters/wof-json-rows.js +45 -0
  99. package/out/src/adapters/wof-json-rows.js.map +1 -0
  100. package/out/src/adapters/wof-postalcode-json/adapter.d.ts +4 -9
  101. package/out/src/adapters/wof-postalcode-json/adapter.d.ts.map +1 -1
  102. package/out/src/adapters/wof-postalcode-json/adapter.js +12 -37
  103. package/out/src/adapters/wof-postalcode-json/adapter.js.map +1 -1
  104. package/out/src/build.d.ts +4 -3
  105. package/out/src/build.d.ts.map +1 -1
  106. package/out/src/build.js +7 -4
  107. package/out/src/build.js.map +1 -1
  108. package/out/src/index.d.ts +1 -1
  109. package/out/src/index.d.ts.map +1 -1
  110. package/out/src/index.js +1 -1
  111. package/out/src/index.js.map +1 -1
  112. package/out/src/parquet-wrapper/reader.d.ts.map +1 -1
  113. package/out/src/parquet-wrapper/reader.js +5 -0
  114. package/out/src/parquet-wrapper/reader.js.map +1 -1
  115. package/out/src/runner.d.ts +2 -2
  116. package/out/src/runner.d.ts.map +1 -1
  117. package/out/src/runner.js +1 -1
  118. package/out/src/runner.js.map +1 -1
  119. package/out/src/shard-recipes/anchor-absorption.d.ts.map +1 -1
  120. package/out/src/shard-recipes/anchor-absorption.js +4 -5
  121. package/out/src/shard-recipes/anchor-absorption.js.map +1 -1
  122. package/out/src/shard-recipes/bare-country.d.ts +25 -0
  123. package/out/src/shard-recipes/bare-country.d.ts.map +1 -0
  124. package/out/src/shard-recipes/bare-country.js +87 -0
  125. package/out/src/shard-recipes/bare-country.js.map +1 -0
  126. package/out/src/shard-recipes/boundary-stress.d.ts.map +1 -1
  127. package/out/src/shard-recipes/boundary-stress.js +2 -2
  128. package/out/src/shard-recipes/boundary-stress.js.map +1 -1
  129. package/out/src/shard-recipes/country-balanced.d.ts.map +1 -1
  130. package/out/src/shard-recipes/country-balanced.js +6 -5
  131. package/out/src/shard-recipes/country-balanced.js.map +1 -1
  132. package/out/src/shard-recipes/cz-pcfirst-preposition.d.ts.map +1 -1
  133. package/out/src/shard-recipes/cz-pcfirst-preposition.js +28 -10
  134. package/out/src/shard-recipes/cz-pcfirst-preposition.js.map +1 -1
  135. package/out/src/shard-recipes/fr-admin-split.d.ts.map +1 -1
  136. package/out/src/shard-recipes/fr-admin-split.js +4 -3
  137. package/out/src/shard-recipes/fr-admin-split.js.map +1 -1
  138. package/out/src/shard-recipes/fr-bare-street.d.ts.map +1 -1
  139. package/out/src/shard-recipes/fr-bare-street.js +72 -11
  140. package/out/src/shard-recipes/fr-bare-street.js.map +1 -1
  141. package/out/src/shard-recipes/fr-lieudit.d.ts.map +1 -1
  142. package/out/src/shard-recipes/fr-lieudit.js +4 -3
  143. package/out/src/shard-recipes/fr-lieudit.js.map +1 -1
  144. package/out/src/shard-recipes/fr-order.d.ts.map +1 -1
  145. package/out/src/shard-recipes/fr-order.js +4 -3
  146. package/out/src/shard-recipes/fr-order.js.map +1 -1
  147. package/out/src/shard-recipes/german.d.ts.map +1 -1
  148. package/out/src/shard-recipes/german.js +3 -3
  149. package/out/src/shard-recipes/german.js.map +1 -1
  150. package/out/src/shard-recipes/house-venue.js +1 -1
  151. package/out/src/shard-recipes/house-venue.js.map +1 -1
  152. package/out/src/shard-recipes/index.d.ts.map +1 -1
  153. package/out/src/shard-recipes/index.js +6 -0
  154. package/out/src/shard-recipes/index.js.map +1 -1
  155. package/out/src/shard-recipes/intersection.d.ts.map +1 -1
  156. package/out/src/shard-recipes/intersection.js +3 -3
  157. package/out/src/shard-recipes/intersection.js.map +1 -1
  158. package/out/src/shard-recipes/locale.d.ts +1 -1
  159. package/out/src/shard-recipes/locale.d.ts.map +1 -1
  160. package/out/src/shard-recipes/locale.js +5 -4
  161. package/out/src/shard-recipes/locale.js.map +1 -1
  162. package/out/src/shard-recipes/no-street.js +1 -1
  163. package/out/src/shard-recipes/no-street.js.map +1 -1
  164. package/out/src/shard-recipes/po-box-cedex.d.ts.map +1 -1
  165. package/out/src/shard-recipes/po-box-cedex.js +2 -2
  166. package/out/src/shard-recipes/po-box-cedex.js.map +1 -1
  167. package/out/src/shard-recipes/po-box.d.ts.map +1 -1
  168. package/out/src/shard-recipes/po-box.js +1 -1
  169. package/out/src/shard-recipes/po-box.js.map +1 -1
  170. package/out/src/shard-recipes/reviewed-postcode-tail.d.ts +47 -0
  171. package/out/src/shard-recipes/reviewed-postcode-tail.d.ts.map +1 -0
  172. package/out/src/shard-recipes/reviewed-postcode-tail.js +125 -0
  173. package/out/src/shard-recipes/reviewed-postcode-tail.js.map +1 -0
  174. package/out/src/shard-recipes/scaffold.d.ts +33 -4
  175. package/out/src/shard-recipes/scaffold.d.ts.map +1 -1
  176. package/out/src/shard-recipes/scaffold.js +5 -6
  177. package/out/src/shard-recipes/scaffold.js.map +1 -1
  178. package/out/src/shard-recipes/street-affix.d.ts.map +1 -1
  179. package/out/src/shard-recipes/street-affix.js +2 -2
  180. package/out/src/shard-recipes/street-affix.js.map +1 -1
  181. package/out/src/shard-recipes/street-bare.js +3 -3
  182. package/out/src/shard-recipes/street-bare.js.map +1 -1
  183. package/out/src/shard-recipes/street.js +2 -2
  184. package/out/src/shard-recipes/street.js.map +1 -1
  185. package/out/src/shard-recipes/sub-venue-sources.d.ts +2 -2
  186. package/out/src/shard-recipes/sub-venue-sources.d.ts.map +1 -1
  187. package/out/src/shard-recipes/sub-venue-sources.js +1 -1
  188. package/out/src/shard-recipes/sub-venue-sources.js.map +1 -1
  189. package/out/src/shard-recipes/sub-venue.d.ts +1 -1
  190. package/out/src/shard-recipes/sub-venue.d.ts.map +1 -1
  191. package/out/src/shard-recipes/sub-venue.js +6 -5
  192. package/out/src/shard-recipes/sub-venue.js.map +1 -1
  193. package/out/src/shard-recipes/trailing-region.d.ts +74 -0
  194. package/out/src/shard-recipes/trailing-region.d.ts.map +1 -0
  195. package/out/src/shard-recipes/trailing-region.js +157 -0
  196. package/out/src/shard-recipes/trailing-region.js.map +1 -0
  197. package/out/src/shard-recipes/unit.d.ts.map +1 -1
  198. package/out/src/shard-recipes/unit.js +2 -2
  199. package/out/src/shard-recipes/unit.js.map +1 -1
  200. package/out/src/synthesizers/boundary-stress.d.ts +1 -1
  201. package/out/src/synthesizers/boundary-stress.d.ts.map +1 -1
  202. package/out/src/synthesizers/german.d.ts +1 -1
  203. package/out/src/synthesizers/german.d.ts.map +1 -1
  204. package/out/src/synthesizers/german.js.map +1 -1
  205. package/out/src/synthesizers/house-venue.d.ts +1 -1
  206. package/out/src/synthesizers/house-venue.d.ts.map +1 -1
  207. package/out/src/synthesizers/house-venue.js +16 -2
  208. package/out/src/synthesizers/house-venue.js.map +1 -1
  209. package/out/src/synthesizers/intersection.d.ts +1 -1
  210. package/out/src/synthesizers/intersection.d.ts.map +1 -1
  211. package/out/src/synthesizers/no-street.d.ts +1 -1
  212. package/out/src/synthesizers/no-street.d.ts.map +1 -1
  213. package/out/src/synthesizers/po-box.d.ts +1 -1
  214. package/out/src/synthesizers/po-box.d.ts.map +1 -1
  215. package/out/src/synthesizers/street.d.ts +1 -1
  216. package/out/src/synthesizers/street.d.ts.map +1 -1
  217. package/out/src/synthesizers/street.js +3 -2
  218. package/out/src/synthesizers/street.js.map +1 -1
  219. package/out/src/synthesizers/utils.d.ts +1 -1
  220. package/out/src/synthesizers/utils.d.ts.map +1 -1
  221. package/out/src/synthesizers/utils.js +3 -2
  222. package/out/src/synthesizers/utils.js.map +1 -1
  223. package/out/src/tools/align-shard.d.ts.map +1 -1
  224. package/out/src/tools/align-shard.js +2 -2
  225. package/out/src/tools/align-shard.js.map +1 -1
  226. package/out/src/tools/fetch/ban.d.ts.map +1 -1
  227. package/out/src/tools/fetch/ban.js +2 -18
  228. package/out/src/tools/fetch/ban.js.map +1 -1
  229. package/out/src/tools/fetch/download.d.ts +12 -3
  230. package/out/src/tools/fetch/download.d.ts.map +1 -1
  231. package/out/src/tools/fetch/download.js +20 -4
  232. package/out/src/tools/fetch/download.js.map +1 -1
  233. package/out/src/tools/fetch/geonames-dump.d.ts +87 -0
  234. package/out/src/tools/fetch/geonames-dump.d.ts.map +1 -0
  235. package/out/src/tools/fetch/geonames-dump.js +178 -0
  236. package/out/src/tools/fetch/geonames-dump.js.map +1 -0
  237. package/out/src/tools/fetch/geonames-postal.d.ts +76 -0
  238. package/out/src/tools/fetch/geonames-postal.d.ts.map +1 -0
  239. package/out/src/tools/fetch/geonames-postal.js +125 -0
  240. package/out/src/tools/fetch/geonames-postal.js.map +1 -0
  241. package/out/src/tools/fetch/imls-pls.d.ts.map +1 -1
  242. package/out/src/tools/fetch/imls-pls.js +2 -2
  243. package/out/src/tools/fetch/imls-pls.js.map +1 -1
  244. package/out/src/tools/fetch/index.d.ts +11 -0
  245. package/out/src/tools/fetch/index.d.ts.map +1 -1
  246. package/out/src/tools/fetch/index.js +11 -0
  247. package/out/src/tools/fetch/index.js.map +1 -1
  248. package/out/src/tools/fetch/nad.d.ts.map +1 -1
  249. package/out/src/tools/fetch/nad.js +20 -11
  250. package/out/src/tools/fetch/nad.js.map +1 -1
  251. package/out/src/tools/fetch/nppes.d.ts.map +1 -1
  252. package/out/src/tools/fetch/nppes.js +9 -7
  253. package/out/src/tools/fetch/nppes.js.map +1 -1
  254. package/out/src/tools/fetch/openaddresses.d.ts.map +1 -1
  255. package/out/src/tools/fetch/openaddresses.js +18 -17
  256. package/out/src/tools/fetch/openaddresses.js.map +1 -1
  257. package/out/src/tools/fetch/ourairports.d.ts.map +1 -1
  258. package/out/src/tools/fetch/ourairports.js +7 -2
  259. package/out/src/tools/fetch/ourairports.js.map +1 -1
  260. package/out/src/tools/fetch/ppd.d.ts +0 -4
  261. package/out/src/tools/fetch/ppd.d.ts.map +1 -1
  262. package/out/src/tools/fetch/ppd.js +1 -1
  263. package/out/src/tools/fetch/ppd.js.map +1 -1
  264. package/out/src/tools/fetch/state-hi-schools.d.ts.map +1 -1
  265. package/out/src/tools/fetch/state-hi-schools.js +3 -19
  266. package/out/src/tools/fetch/state-hi-schools.js.map +1 -1
  267. package/out/src/tools/fetch/state-sources.d.ts +4 -0
  268. package/out/src/tools/fetch/state-sources.d.ts.map +1 -1
  269. package/out/src/tools/fetch/state-sources.js +1 -5
  270. package/out/src/tools/fetch/state-sources.js.map +1 -1
  271. package/out/src/tools/fetch/tiger-full.d.ts.map +1 -1
  272. package/out/src/tools/fetch/tiger-full.js +15 -21
  273. package/out/src/tools/fetch/tiger-full.js.map +1 -1
  274. package/out/src/tools/golden-expand.d.ts.map +1 -1
  275. package/out/src/tools/golden-expand.js +34 -25
  276. package/out/src/tools/golden-expand.js.map +1 -1
  277. package/out/src/tools/golden-relabel-street.js +2 -2
  278. package/out/src/tools/golden-relabel-street.js.map +1 -1
  279. package/out/src/tools/index.d.ts +2 -2
  280. package/out/src/tools/index.d.ts.map +1 -1
  281. package/out/src/tools/index.js +2 -2
  282. package/out/src/tools/index.js.map +1 -1
  283. package/out/src/tools/ingest-csv.d.ts.map +1 -1
  284. package/out/src/tools/ingest-csv.js +0 -11
  285. package/out/src/tools/ingest-csv.js.map +1 -1
  286. package/out/src/tools/overlay-manifest.d.ts +4 -0
  287. package/out/src/tools/overlay-manifest.d.ts.map +1 -1
  288. package/out/src/tools/overlay-manifest.js +16 -6
  289. package/out/src/tools/overlay-manifest.js.map +1 -1
  290. package/out/src/tools/postcode-triples.d.ts +175 -0
  291. package/out/src/tools/postcode-triples.d.ts.map +1 -0
  292. package/out/src/tools/postcode-triples.js +304 -0
  293. package/out/src/tools/postcode-triples.js.map +1 -0
  294. package/out/src/tools/shard-kryptonite.d.ts.map +1 -1
  295. package/out/src/tools/shard-kryptonite.js +1 -2
  296. package/out/src/tools/shard-kryptonite.js.map +1 -1
  297. package/out/src/tools/shard-translit.d.ts.map +1 -1
  298. package/out/src/tools/shard-translit.js +4 -26
  299. package/out/src/tools/shard-translit.js.map +1 -1
  300. package/out/src/tools/sub-venue/harvest.d.ts +100 -0
  301. package/out/src/tools/sub-venue/harvest.d.ts.map +1 -0
  302. package/out/src/tools/sub-venue/harvest.js +168 -0
  303. package/out/src/tools/sub-venue/harvest.js.map +1 -0
  304. package/out/src/tools/sub-venue/head-nouns.d.ts +50 -0
  305. package/out/src/tools/sub-venue/head-nouns.d.ts.map +1 -0
  306. package/out/src/tools/sub-venue/head-nouns.js +210 -0
  307. package/out/src/tools/sub-venue/head-nouns.js.map +1 -0
  308. package/out/src/tools/sub-venue/surfaces.d.ts +45 -0
  309. package/out/src/tools/sub-venue/surfaces.d.ts.map +1 -0
  310. package/out/src/tools/sub-venue/surfaces.js +77 -0
  311. package/out/src/tools/sub-venue/surfaces.js.map +1 -0
  312. package/out/src/tools/sub-venue/table.d.ts +229 -0
  313. package/out/src/tools/sub-venue/table.d.ts.map +1 -0
  314. package/out/src/tools/sub-venue/table.js +108 -0
  315. package/out/src/tools/sub-venue/table.js.map +1 -0
  316. package/out/src/tools/sub-venue/wikidata.d.ts +26 -0
  317. package/out/src/tools/sub-venue/wikidata.d.ts.map +1 -0
  318. package/out/src/tools/sub-venue/wikidata.js +62 -0
  319. package/out/src/tools/sub-venue/wikidata.js.map +1 -0
  320. package/out/src/tools/sub-venue-lexicon.d.ts +27 -378
  321. package/out/src/tools/sub-venue-lexicon.d.ts.map +1 -1
  322. package/out/src/tools/sub-venue-lexicon.js +30 -565
  323. package/out/src/tools/sub-venue-lexicon.js.map +1 -1
  324. package/out/src/utils/align.d.ts +1 -1
  325. package/out/src/utils/align.d.ts.map +1 -1
  326. package/out/src/utils/align.js.map +1 -1
  327. package/out/src/utils/golden.js +1 -1
  328. package/out/src/utils/golden.js.map +1 -1
  329. package/out/src/utils/license.js +1 -1
  330. package/out/src/utils/license.js.map +1 -1
  331. package/out/src/utils/parquet.d.ts +24 -5
  332. package/out/src/utils/parquet.d.ts.map +1 -1
  333. package/out/src/utils/parquet.js +6 -2
  334. package/out/src/utils/parquet.js.map +1 -1
  335. package/out/src/utils/split.d.ts +1 -1
  336. package/out/src/utils/split.d.ts.map +1 -1
  337. package/out/src/utils/split.js +2 -2
  338. package/out/src/utils/split.js.map +1 -1
  339. package/package.json +230 -10
  340. package/src/adapters/ban/adapter.ts +6 -4
  341. package/src/adapters/fcc-bdc/adapter.ts +4 -3
  342. package/src/adapters/geonames/adapter.ts +3 -2
  343. package/src/adapters/geonames-postal/adapter.ts +4 -3
  344. package/src/adapters/gnaf/adapter.ts +3 -2
  345. package/src/adapters/index.ts +2 -2
  346. package/src/adapters/openaddresses/adapter.ts +4 -3
  347. package/src/adapters/overture/adapter.ts +3 -2
  348. package/src/adapters/state-hi-schools/adapter.ts +8 -6
  349. package/src/adapters/state-ia-contractors/adapter.ts +13 -7
  350. package/src/adapters/state-ny-notaries/adapter.ts +13 -31
  351. package/src/adapters/state-tx-notaries/adapter.ts +13 -7
  352. package/src/adapters/synth-po-box/adapter.ts +5 -4
  353. package/src/adapters/tiger/adapter.ts +3 -2
  354. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +8 -6
  355. package/src/adapters/usgov-imls-pls/adapter.ts +12 -7
  356. package/src/adapters/usgov-irs-bmf/adapter.ts +7 -5
  357. package/src/adapters/usgov-nad/adapter.ts +9 -8
  358. package/src/adapters/usgov-nppes/adapter.ts +13 -11
  359. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +8 -6
  360. package/src/adapters/utils/index.ts +16 -3
  361. package/src/adapters/wof-admin-jp/adapter.ts +9 -12
  362. package/src/adapters/wof-admin-json/adapter.ts +16 -54
  363. package/src/adapters/wof-json-rows.ts +79 -0
  364. package/src/adapters/wof-postalcode-json/adapter.ts +17 -50
  365. package/src/build.ts +10 -10
  366. package/src/index.ts +1 -1
  367. package/src/parquet-wrapper/reader.ts +9 -0
  368. package/src/runner.ts +2 -7
  369. package/src/shard-recipes/anchor-absorption.ts +5 -5
  370. package/src/shard-recipes/bare-country.ts +95 -0
  371. package/src/shard-recipes/boundary-stress.ts +2 -5
  372. package/src/shard-recipes/country-balanced.ts +8 -6
  373. package/src/shard-recipes/cz-pcfirst-preposition.ts +29 -10
  374. package/src/shard-recipes/fr-admin-split.ts +6 -4
  375. package/src/shard-recipes/fr-bare-street.ts +85 -10
  376. package/src/shard-recipes/fr-lieudit.ts +6 -4
  377. package/src/shard-recipes/fr-order.ts +6 -4
  378. package/src/shard-recipes/german.ts +4 -3
  379. package/src/shard-recipes/house-venue.ts +1 -1
  380. package/src/shard-recipes/index.ts +6 -0
  381. package/src/shard-recipes/intersection.ts +5 -4
  382. package/src/shard-recipes/locale.ts +6 -8
  383. package/src/shard-recipes/no-street.ts +1 -1
  384. package/src/shard-recipes/po-box-cedex.ts +4 -3
  385. package/src/shard-recipes/po-box.ts +1 -5
  386. package/src/shard-recipes/reviewed-postcode-tail.ts +189 -0
  387. package/src/shard-recipes/scaffold.ts +38 -7
  388. package/src/shard-recipes/street-affix.ts +4 -3
  389. package/src/shard-recipes/street-bare.ts +3 -3
  390. package/src/shard-recipes/street.ts +2 -2
  391. package/src/shard-recipes/sub-venue-sources.ts +4 -14
  392. package/src/shard-recipes/sub-venue.ts +10 -8
  393. package/src/shard-recipes/trailing-region.ts +183 -0
  394. package/src/shard-recipes/unit.ts +4 -3
  395. package/src/synthesizers/boundary-stress.ts +1 -1
  396. package/src/synthesizers/german.ts +2 -1
  397. package/src/synthesizers/house-venue.ts +17 -3
  398. package/src/synthesizers/intersection.ts +1 -1
  399. package/src/synthesizers/no-street.ts +1 -1
  400. package/src/synthesizers/po-box.ts +1 -1
  401. package/src/synthesizers/street.ts +5 -3
  402. package/src/synthesizers/utils.ts +5 -3
  403. package/src/tools/align-shard.ts +3 -2
  404. package/src/tools/fetch/ban.ts +2 -22
  405. package/src/tools/fetch/download.ts +22 -4
  406. package/src/tools/fetch/geonames-dump.ts +267 -0
  407. package/src/tools/fetch/geonames-postal.ts +180 -0
  408. package/src/tools/fetch/imls-pls.ts +2 -2
  409. package/src/tools/fetch/index.ts +11 -0
  410. package/src/tools/fetch/nad.ts +21 -10
  411. package/src/tools/fetch/nppes.ts +8 -6
  412. package/src/tools/fetch/openaddresses.ts +18 -22
  413. package/src/tools/fetch/ourairports.ts +7 -2
  414. package/src/tools/fetch/ppd.ts +1 -1
  415. package/src/tools/fetch/state-hi-schools.ts +3 -23
  416. package/src/tools/fetch/state-sources.ts +1 -1
  417. package/src/tools/fetch/tiger-full.ts +17 -24
  418. package/src/tools/golden-expand.ts +44 -33
  419. package/src/tools/golden-relabel-street.ts +2 -2
  420. package/src/tools/index.ts +2 -2
  421. package/src/tools/ingest-csv.ts +0 -13
  422. package/src/tools/overlay-manifest.ts +20 -8
  423. package/src/tools/postcode-triples.ts +375 -0
  424. package/src/tools/shard-kryptonite.ts +5 -4
  425. package/src/tools/shard-translit.ts +10 -35
  426. package/src/tools/sub-venue/harvest.ts +235 -0
  427. package/src/tools/sub-venue/head-nouns.ts +242 -0
  428. package/src/tools/sub-venue/surfaces.ts +95 -0
  429. package/src/tools/sub-venue/table.ts +281 -0
  430. package/src/tools/sub-venue/wikidata.ts +85 -0
  431. package/src/tools/sub-venue-lexicon.ts +67 -886
  432. package/src/utils/align.ts +2 -1
  433. package/src/utils/golden.ts +1 -1
  434. package/src/utils/license.ts +1 -1
  435. package/src/utils/parquet.ts +22 -8
  436. package/src/utils/split.ts +4 -3
@@ -0,0 +1,267 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Fetch GeoNames per-country GAZETTEER dumps — the 19-column `<CC>.txt` files under
7
+ * `https://download.geonames.org/export/dump/` (NOT the postal exports; those are `export/zip/` and
8
+ * `geonames-postal.ts`'s job). The dumps carry feature classes and codes (column 8: `PPLC` national capital,
9
+ * `PPLA` first-order administrative seat), which is what the capitals reference build consumes (#1880).
10
+ *
11
+ * The catalog question is answered by the SOURCE, not by an ISO list: `countryInfo.txt` in the same directory
12
+ * enumerates every country GeoNames publishes, one row per ISO alpha-2 code, and also names each country's
13
+ * capital — the cross-check the capitals build grades its `PPLC` extraction against. Fetch that first; derive
14
+ * the country set from it; then a dump absent from disk is a measured gap against the source's own catalog
15
+ * rather than a silent hole. The dump directory may hold files this tool did not fetch: present files are never
16
+ * overwritten, and a present `<CC>.txt` that is NOT a 19-column gazetteer dump (GeoNames' postal exports share
17
+ * the basename) is reported as `wrong_format_present`, never counted as coverage.
18
+ */
19
+
20
+ import { existsSync, mkdirSync } from "node:fs"
21
+ import { open, readFile, rm } from "node:fs/promises"
22
+ import { join } from "node:path"
23
+
24
+ import { extractZipEntry } from "@mailwoman/core/fs/zip"
25
+ import { sha256File } from "@mailwoman/core/utils"
26
+
27
+ import type { BaseFetchOptions, FetchSummary } from "./download.ts"
28
+ import { downloadToFile, HTTPStatusError, writeManifest } from "./download.ts"
29
+
30
+ /**
31
+ * The one status that means "the source does not publish this country" rather than "the transfer failed".
32
+ */
33
+ const HTTP_NOT_FOUND = 404
34
+
35
+ const SLUG = "geonames-dump"
36
+
37
+ /**
38
+ * GeoNames' gazetteer-dump directory — one zip per ISO alpha-2 code holding `<CC>.txt`, plus `countryInfo.txt` as a
39
+ * bare text file.
40
+ */
41
+ const BASE_URL = "https://download.geonames.org/export/dump"
42
+
43
+ export interface FetchGeonamesDumpOptions extends BaseFetchOptions {
44
+ /**
45
+ * ISO alpha-2 codes, any casing. Absent → every country `countryInfo.txt` enumerates.
46
+ */
47
+ countries?: readonly string[]
48
+ /**
49
+ * Dump directory to read from. Defaults to GeoNames' own; exists so the 404 and coverage behaviour can be exercised
50
+ * against a local server.
51
+ */
52
+ baseURL?: string
53
+ /**
54
+ * Refetch a dump whose `<CC>.txt` already exists. Default false — the tool fills gaps.
55
+ */
56
+ force?: boolean
57
+ }
58
+
59
+ interface GeonamesDumpFileEntry {
60
+ country: string
61
+ filename: string
62
+ source_url: string
63
+ sha256: string
64
+ bytes: number
65
+ }
66
+
67
+ export interface GeonamesDumpManifest {
68
+ source: string
69
+ base_url: string
70
+ license: string
71
+ attribution: string
72
+ downloaded_at: string
73
+ files: GeonamesDumpFileEntry[]
74
+ /**
75
+ * `<CC>.txt` files already on disk and left alone — the hand-fetched population this tool extends.
76
+ */
77
+ skipped_present: string[]
78
+ /**
79
+ * Countries in the source catalog that the source's dump directory nonetheless 404s — a fact about the source,
80
+ * recorded so a later reader does not spend the fetch to rediscover it.
81
+ */
82
+ unavailable: string[]
83
+ /**
84
+ * Present `<CC>.txt` files that are NOT 19-column gazetteer dumps — GeoNames' postal exports share the same basename,
85
+ * and seven tier-1 postal files sat at these paths reading as "present" until the capitals build found them
86
+ * capital-less. Left in place (this tool never overwrites data it did not fetch); the fix is to move the file to its
87
+ * own home and rerun.
88
+ */
89
+ wrong_format_present: string[]
90
+ }
91
+
92
+ /**
93
+ * Column count of a gazetteer dump row — the discriminator against GeoNames' 12-column postal exports, which share the
94
+ * `<CC>.txt` basename.
95
+ */
96
+ const GAZETTEER_DUMP_COLUMNS = 19
97
+
98
+ /**
99
+ * True when the first non-empty line carries the gazetteer dump's 19 tab-separated columns. Accepts a partial head read
100
+ * — the first line is the whole question, so callers need not hand it a resident 350 MB dump.
101
+ */
102
+ export function looksLikeGazetteerDump(text: string): boolean {
103
+ let start = 0
104
+
105
+ while (start < text.length) {
106
+ const end = text.indexOf("\n", start)
107
+ const line = end === -1 ? text.slice(start) : text.slice(start, end)
108
+
109
+ if (line.trim()) {
110
+ let tabs = 0
111
+
112
+ for (let i = line.indexOf("\t"); i !== -1; i = line.indexOf("\t", i + 1)) {
113
+ tabs++
114
+ }
115
+
116
+ return tabs === GAZETTEER_DUMP_COLUMNS - 1
117
+ }
118
+
119
+ if (end === -1) break
120
+
121
+ start = end + 1
122
+ }
123
+
124
+ return false
125
+ }
126
+
127
+ /**
128
+ * Parse the ISO codes (column 1) and capital names (column 6) out of `countryInfo.txt` — `#`-prefixed lines are the
129
+ * file's own commentary.
130
+ */
131
+ export function parseCountryInfo(text: string): Array<{ country: string; capital: string }> {
132
+ const rows: Array<{ country: string; capital: string }> = []
133
+
134
+ // oxlint-disable-next-line mailwoman/prefer-spliterator -- countryInfo.txt is ~40 KB, one row per country on Earth; it cannot grow past that
135
+ for (const line of text.split("\n")) {
136
+ if (line.startsWith("#") || !line.trim()) continue
137
+
138
+ // oxlint-disable-next-line mailwoman/prefer-spliterator -- one bounded 19-column catalog row
139
+ const cols = line.split("\t")
140
+ const country = cols[0]?.trim().toUpperCase()
141
+
142
+ if (country?.length === 2) {
143
+ rows.push({ country, capital: cols[5]?.trim() ?? "" })
144
+ }
145
+ }
146
+
147
+ return rows
148
+ }
149
+
150
+ /**
151
+ * Bytes read to classify a present file: enough to cover a first dump row whose alternate-names column runs long (they
152
+ * reach several KB), a fraction of the largest dumps (US.txt is ~350 MB).
153
+ */
154
+ const FORMAT_SNIFF_BYTES = 65_536
155
+
156
+ async function readFileHead(path: string): Promise<string> {
157
+ const handle = await open(path, "r")
158
+
159
+ try {
160
+ const buffer = Buffer.alloc(FORMAT_SNIFF_BYTES)
161
+ const { bytesRead } = await handle.read(buffer, 0, FORMAT_SNIFF_BYTES, 0)
162
+
163
+ return buffer.subarray(0, bytesRead).toString("utf8")
164
+ } finally {
165
+ await handle.close()
166
+ }
167
+ }
168
+
169
+ /**
170
+ * Download `countryInfo.txt` plus every missing `<CC>.zip`, extracting each to `<outRoot>/<CC>.txt` beside the
171
+ * hand-fetched dumps, with a `MANIFEST.json` naming fetched, skipped-present, and source-unavailable countries.
172
+ */
173
+ export async function fetchGeonamesDumps(
174
+ options: FetchGeonamesDumpOptions,
175
+ report?: (line: string) => void
176
+ ): Promise<FetchSummary & { skippedPresent: string[] }> {
177
+ mkdirSync(options.outRoot, { recursive: true })
178
+
179
+ const baseURL = options.baseURL ?? BASE_URL
180
+ const countryInfoDest = join(options.outRoot, "countryInfo.txt")
181
+
182
+ await downloadToFile({
183
+ url: `${baseURL}/countryInfo.txt`,
184
+ dest: countryInfoDest,
185
+ timeoutMs: 120_000,
186
+ retries: 2,
187
+ report,
188
+ })
189
+
190
+ const catalog = parseCountryInfo(await readFile(countryInfoDest, "utf8"))
191
+ const countries = options.countries?.map((code) => code.trim().toUpperCase()) ?? catalog.map((row) => row.country)
192
+
193
+ const entries: GeonamesDumpFileEntry[] = []
194
+ const failedCodes: string[] = []
195
+ const unavailable: string[] = []
196
+ const skippedPresent: string[] = []
197
+ const wrongFormatPresent: string[] = []
198
+ let fetched = 0
199
+
200
+ for (const country of countries) {
201
+ const txtDest = join(options.outRoot, `${country}.txt`)
202
+
203
+ if (!options.force && existsSync(txtDest)) {
204
+ if (looksLikeGazetteerDump(await readFileHead(txtDest))) {
205
+ skippedPresent.push(country)
206
+ } else {
207
+ report?.(`✗ ${country}.txt is present but is not a 19-column gazetteer dump — move it aside and rerun`)
208
+ wrongFormatPresent.push(country)
209
+ }
210
+
211
+ continue
212
+ }
213
+
214
+ const filename = `${country}.zip`
215
+ const url = `${baseURL}/${filename}`
216
+ const zipDest = join(options.outRoot, filename)
217
+
218
+ report?.(`=== ${SLUG} / ${country}`)
219
+
220
+ try {
221
+ await downloadToFile({ url, dest: zipDest, timeoutMs: 300_000, retries: 2, report })
222
+ await extractZipEntry(zipDest, `${country}.txt`, txtDest)
223
+ await rm(zipDest, { force: true })
224
+
225
+ entries.push({
226
+ country,
227
+ filename: `${country}.txt`,
228
+ source_url: url,
229
+ sha256: await sha256File(txtDest),
230
+ bytes: (await readFile(txtDest)).byteLength,
231
+ })
232
+
233
+ fetched++
234
+ } catch (error) {
235
+ await rm(zipDest, { force: true })
236
+
237
+ const message = error instanceof Error ? error.message : String(error)
238
+
239
+ // Branch on the TYPED status (the geonames-postal lesson): message prose contains the URL, and a URL
240
+ // can contain any substring.
241
+ if (error instanceof HTTPStatusError && error.status === HTTP_NOT_FOUND) {
242
+ report?.(`✗ ${country}: GeoNames publishes no gazetteer dump for this country`)
243
+ unavailable.push(country)
244
+ } else {
245
+ report?.(`✗ ${country}: ${message}`)
246
+ }
247
+
248
+ failedCodes.push(country)
249
+ }
250
+ }
251
+
252
+ const manifest: GeonamesDumpManifest = {
253
+ source: SLUG,
254
+ base_url: baseURL,
255
+ license: "CC-BY-4.0",
256
+ attribution: "GeoNames",
257
+ downloaded_at: new Date().toISOString(),
258
+ files: entries,
259
+ skipped_present: skippedPresent.toSorted(),
260
+ unavailable,
261
+ wrong_format_present: wrongFormatPresent.toSorted(),
262
+ }
263
+
264
+ await writeManifest(join(options.outRoot, "MANIFEST.json"), manifest)
265
+
266
+ return { fetched, skipped: skippedPresent.length, failed: failedCodes.length, failedCodes, skippedPresent }
267
+ }
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Fetch the GeoNames per-country postal-code exports — the only source in this family that carries a
7
+ * `(postcode, locality, region)` triple with the NAMES inline.
8
+ *
9
+ * Source : https://download.geonames.org/export/zip/<CC>.zip
10
+ * License: CC-BY-4.0, attribute "GeoNames". Tier B.
11
+ *
12
+ * ## Why this source and not a join
13
+ *
14
+ * A postcode shard is only useful to the corpus if a postcode reaches a locality and a region. Two routes exist and
15
+ * only one of them works everywhere:
16
+ *
17
+ * - **`parent_id`** — `postalcode-intl.db` carries a real parent that resolves in the admin gazetteer (measured: NL
18
+ * 97.5% of rows linked, FR 90.7%, DE 66.1%, ES 34.9%, IT 27.4%, and 93.8–100% of those resolve to a
19
+ * `locality`/`localadmin`). It is also the ONLY shard that does: `postalcode-geonames-intl.db` and every
20
+ * `postalcode-<cc>-overture.db` carry `parent_id = 0` on every row, so five countries have this route and the rest
21
+ * have none.
22
+ * - **Nearest locality centroid** — the obvious fallback, and it does not work. Scored against the `parent_id` truth
23
+ * on 1,000–1,500 postcodes per country: NL 81.1%, DE 44.7%, ES 35.1%, FR 29.0%, IT 13.5%. A locality's centroid
24
+ * sits at its middle, so a postcode near the edge is routinely closer to a neighbouring town's centroid than to its
25
+ * own. Under 50% for four of the five, which is not a join.
26
+ *
27
+ * The GeoNames export sidesteps both: columns 3 and 4 ARE the place and admin1 names, so there is nothing to join and
28
+ * nothing to approximate. `@mailwoman/corpus`'s `geonames-postal` adapter consumes it directly.
29
+ *
30
+ * ## Coverage is not universal, and the gap is the point
31
+ *
32
+ * GeoNames publishes ~80 countries, NOT all of them. Venezuela returns 404 — so a VE postcode shard cannot be built
33
+ * from this source at any effort, and that is an acquisition question rather than a build one. Ask for a country
34
+ * before assuming it is there; an absent country fails as one entry, never as the whole run.
35
+ *
36
+ * ## Row counts from this source overstate, for some countries by exactly 2×
37
+ *
38
+ * Countries whose postcode format contains a hyphen are published TWICE — once `3750-000`, once `3750000`. Measured
39
+ * on the shard built from this source: PT 395,544 rows over 197,772 distinct codes and PL 40,598 over 20,299, both
40
+ * exactly 2.00×, while AU, CZ and AT (no hyphen in the format) are 1.00×. A consumer sizing a shard from the row
41
+ * count doubles its estimate for those countries.
42
+ *
43
+ * ## Why `downloadToFile` and not `APIClient`
44
+ *
45
+ * Same split `AGENTS.md` draws and the `ourairports` sibling explains: these are static file transfers from a plain
46
+ * file host, run once per refresh. The pacing, retry and caching `APIClient` exists for have nothing to act on here.
47
+ *
48
+ * Invoke via `mailwoman corpus fetch geonames-postal --countries pt,au,nz`.
49
+ */
50
+
51
+ import { mkdirSync } from "node:fs"
52
+ import { join } from "node:path"
53
+
54
+ import { sha256File } from "@mailwoman/core/utils"
55
+
56
+ import type { BaseFetchOptions, FetchSummary } from "./download.ts"
57
+ import { downloadToFile, HTTPStatusError, writeManifest } from "./download.ts"
58
+
59
+ /**
60
+ * The one status that means "the source does not publish this country" rather than "the transfer failed".
61
+ */
62
+ const HTTP_NOT_FOUND = 404
63
+
64
+ const SLUG = "geonames-postal"
65
+
66
+ /**
67
+ * GeoNames' own export directory. One zip per ISO alpha-2 code, each holding `<CC>.txt` plus the shared `readme.txt`.
68
+ */
69
+ const BASE_URL = "https://download.geonames.org/export/zip"
70
+
71
+ /**
72
+ * Countries fetched when the caller names none.
73
+ *
74
+ * These are the ones the corpus wants and cannot get from `postalcode-intl.db`'s `parent_id` route — see the header for
75
+ * why that route covers exactly five countries. Venezuela is deliberately absent because GeoNames does not publish it.
76
+ */
77
+ export const GEONAMES_POSTAL_DEFAULT_COUNTRIES = ["PT", "AU", "NZ", "IE", "BR", "ZA", "MX"] as const
78
+
79
+ export interface FetchGeonamesPostalOptions extends BaseFetchOptions {
80
+ /**
81
+ * ISO alpha-2 codes, in any casing. Defaults to {@linkcode GEONAMES_POSTAL_DEFAULT_COUNTRIES}.
82
+ */
83
+ countries?: readonly string[]
84
+ /**
85
+ * Export directory to read from. Defaults to GeoNames' own. Exists so the 404-is-a-coverage-finding behaviour can be
86
+ * exercised against a local server rather than by asking GeoNames for a country it does not have.
87
+ */
88
+ baseURL?: string
89
+ }
90
+
91
+ interface GeonamesPostalFileEntry {
92
+ country: string
93
+ filename: string
94
+ source_url: string
95
+ sha256: string
96
+ bytes: number
97
+ }
98
+
99
+ interface GeonamesPostalManifest {
100
+ source: string
101
+ base_url: string
102
+ license: string
103
+ attribution: string
104
+ downloaded_at: string
105
+ files: GeonamesPostalFileEntry[]
106
+ /**
107
+ * Countries asked for and NOT published by GeoNames, recorded so a later reader does not spend the fetch again to
108
+ * rediscover it. An absence here is a fact about the source, not about the run.
109
+ */
110
+ unavailable: string[]
111
+ }
112
+
113
+ /**
114
+ * Download the requested GeoNames postal zips into `<outRoot>/geonames-postal/`, with a sibling `MANIFEST.json`
115
+ * carrying each file's origin URL, sha256 and byte count, plus the countries the source does not publish.
116
+ *
117
+ * A country the source does not carry is counted as failed and named in `failedCodes` — it does not stop the rest.
118
+ */
119
+ export async function fetchGeonamesPostal(
120
+ options: FetchGeonamesPostalOptions,
121
+ report?: (line: string) => void
122
+ ): Promise<FetchSummary> {
123
+ const destDir = join(options.outRoot, SLUG)
124
+ mkdirSync(destDir, { recursive: true })
125
+
126
+ const countries = (options.countries ?? GEONAMES_POSTAL_DEFAULT_COUNTRIES).map((code) => code.trim().toUpperCase())
127
+ const baseURL = options.baseURL ?? BASE_URL
128
+ const entries: GeonamesPostalFileEntry[] = []
129
+ const failedCodes: string[] = []
130
+ const unavailable: string[] = []
131
+ let fetched = 0
132
+ let failed = 0
133
+
134
+ for (const country of countries) {
135
+ const filename = `${country}.zip`
136
+ const url = `${baseURL}/${filename}`
137
+ const dest = join(destDir, filename)
138
+
139
+ report?.(`=== ${SLUG} / ${country}`)
140
+
141
+ try {
142
+ const { bytes } = await downloadToFile({ url, dest, timeoutMs: 300_000, retries: 2, report })
143
+
144
+ entries.push({ country, filename, source_url: url, sha256: await sha256File(dest), bytes })
145
+
146
+ fetched++
147
+ } catch (error) {
148
+ const message = error instanceof Error ? error.message : String(error)
149
+
150
+ // A 404 here means GeoNames does not publish the country at all, which is a different finding from a failed
151
+ // transfer and the one a caller planning a shard needs to see. Branch on the TYPED status: matching message
152
+ // prose classified a 500 as "unpublished" whenever the URL happened to contain the substring 404 — an
153
+ // ephemeral test-server port did exactly that in CI.
154
+ if (error instanceof HTTPStatusError && error.status === HTTP_NOT_FOUND) {
155
+ report?.(`✗ ${country}: GeoNames does not publish a postal export for this country`)
156
+ unavailable.push(country)
157
+ } else {
158
+ report?.(`✗ ${country}: ${message}`)
159
+ }
160
+
161
+ failedCodes.push(country)
162
+
163
+ failed++
164
+ }
165
+ }
166
+
167
+ const manifest: GeonamesPostalManifest = {
168
+ source: "GeoNames postal codes",
169
+ base_url: baseURL,
170
+ license: "CC-BY-4.0",
171
+ attribution: "GeoNames",
172
+ downloaded_at: new Date().toISOString(),
173
+ files: entries,
174
+ unavailable,
175
+ }
176
+
177
+ await writeManifest(join(destDir, "MANIFEST.json"), manifest)
178
+
179
+ return { fetched, skipped: 0, failed, failedCodes }
180
+ }
@@ -27,6 +27,7 @@ import { existsSync, mkdirSync, statSync } from "node:fs"
27
27
  import { rm } from "node:fs/promises"
28
28
  import { basename, join } from "node:path"
29
29
 
30
+ import { BYTES_PER_KIB, ByteFormatter } from "@mailwoman/core/fs/utils"
30
31
  import { extractZipEntry, listZipEntries } from "@mailwoman/core/fs/zip"
31
32
  import { sha256File } from "@mailwoman/core/utils"
32
33
 
@@ -37,7 +38,6 @@ import { downloadToFile, readManifest, writeManifest } from "./download.ts"
37
38
  * Bytes per KiB — the divisor for human-readable sizes, and the floor below which a "download" is an error page rather
38
39
  * than data.
39
40
  */
40
- const BYTES_PER_KIB = 1024
41
41
 
42
42
  /**
43
43
  * The PLS FY 2023 bulk CSV ZIP (most recent as of 2026-05). If IMLS publishes a newer year, update this URL.
@@ -93,7 +93,7 @@ export async function fetchIMLSPLS(
93
93
  report,
94
94
  })
95
95
 
96
- report?.(` Downloaded: ${(zipSize / 1024 / 1024).toFixed(1)} MB`)
96
+ report?.(` Downloaded: ${ByteFormatter.formatIEC(zipSize)}`)
97
97
 
98
98
  if (zipSize < BYTES_PER_KIB) {
99
99
  report?.(` ✗ Response too small (${zipSize} bytes) — probable error page`)
@@ -33,6 +33,11 @@
33
33
  * Ouverte 2.0).
34
34
  * - `nad` — US DOT National Address Database (~97M address points, ArcGIS FeatureServer). Tier A
35
35
  * (US PD).
36
+ * - `geonames-postal` — GeoNames per-country postal exports (~80 countries). Tier B (CC-BY-4.0,
37
+ * attribute "GeoNames"). The only source in this family carrying `(postcode, locality, region)`
38
+ * with the names INLINE, which is why it exists: the `parent_id` join route covers exactly five
39
+ * countries and the nearest-centroid fallback measured under 50% agreement on four of those five.
40
+ * GeoNames does not publish every country — Venezuela 404s.
36
41
  * - `hrsa` — HRSA Health Center Service Delivery Sites (federal). Tier A (US PD).
37
42
  * - `imls-pls` — IMLS Public Libraries Survey, outlet-level (~17K library branches, FY 2023).
38
43
  * Tier A (US PD).
@@ -93,6 +98,8 @@
93
98
  */
94
99
 
95
100
  import { fetchBan } from "./ban.ts"
101
+ import { fetchGeonamesDumps } from "./geonames-dump.ts"
102
+ import { fetchGeonamesPostal } from "./geonames-postal.ts"
96
103
  import { fetchHRSA } from "./hrsa.ts"
97
104
  import { fetchIMLSPLS } from "./imls-pls.ts"
98
105
  import { fetchNAD } from "./nad.ts"
@@ -105,6 +112,8 @@ import { fetchTigerFull } from "./tiger-full.ts"
105
112
  import { fetchWikidataSubVenue } from "./wikidata-subvenue.ts"
106
113
 
107
114
  export * from "./ban.ts"
115
+ export * from "./geonames-dump.ts"
116
+ export * from "./geonames-postal.ts"
108
117
  export * from "./hrsa.ts"
109
118
  export * from "./imls-pls.ts"
110
119
  export * from "./nad.ts"
@@ -122,6 +131,8 @@ export * from "./wikidata-subvenue.ts"
122
131
  export const FETCH_SOURCES = {
123
132
  ban: fetchBan,
124
133
  nad: fetchNAD,
134
+ "geonames-dump": fetchGeonamesDumps,
135
+ "geonames-postal": fetchGeonamesPostal,
125
136
  hrsa: fetchHRSA,
126
137
  "imls-pls": fetchIMLSPLS,
127
138
  nppes: fetchNPPES,
@@ -40,6 +40,7 @@ import { existsSync, mkdirSync, statSync } from "node:fs"
40
40
  import { writeFile } from "node:fs/promises"
41
41
  import { join } from "node:path"
42
42
 
43
+ import { APIClient, pluckResponseData } from "@mailwoman/core/api"
43
44
  import { sha256File } from "@mailwoman/core/utils"
44
45
 
45
46
  import type { BaseFetchOptions, FetchSummary } from "./download.ts"
@@ -93,6 +94,17 @@ interface ChunkManifest {
93
94
  complete: boolean
94
95
  }
95
96
 
97
+ /**
98
+ * ArcGIS paged reads. Retry is ON: the loop walks OBJECTID ranges to completion, so one throttled page previously ended
99
+ * a multi-hour national download. No rate budget — pages are requested one at a time and each assembles thousands of
100
+ * records server-side.
101
+ */
102
+ const nadClient = new APIClient({
103
+ displayName: "nad",
104
+ retry: true,
105
+ axios: { headers: { "Accept-Encoding": "gzip, br" } },
106
+ })
107
+
96
108
  async function fetchPage(startOID: number, endOID: number, pageSize: number): Promise<unknown[]> {
97
109
  const url = new URL(`${FEATURE_SERVICE_URL}/query`)
98
110
  url.searchParams.set("where", `OBJECTID BETWEEN ${startOID} AND ${endOID}`)
@@ -100,13 +112,12 @@ async function fetchPage(startOID: number, endOID: number, pageSize: number): Pr
100
112
  url.searchParams.set("f", "json")
101
113
  url.searchParams.set("resultRecordCount", String(pageSize))
102
114
 
103
- const res = await fetch(url, {
104
- headers: { "Accept-Encoding": "gzip, br" },
105
- signal: AbortSignal.timeout(120_000),
106
- })
107
-
108
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} on OID ${startOID}-${endOID}`)
109
- const data = (await res.json()) as { features?: Array<{ attributes: unknown }>; error?: { message: string } }
115
+ const data = await nadClient
116
+ .fetch<{ features?: Array<{ attributes: unknown }>; error?: { message: string } }>({
117
+ url: url.toString(),
118
+ timeout: 120_000,
119
+ })
120
+ .then(pluckResponseData)
110
121
 
111
122
  if (data.error) throw new Error(`ArcGIS error on OID ${startOID}-${endOID}: ${data.error.message}`)
112
123
 
@@ -118,10 +129,10 @@ async function discoverTotalCount(): Promise<number> {
118
129
  url.searchParams.set("where", "1=1")
119
130
  url.searchParams.set("returnCountOnly", "true")
120
131
  url.searchParams.set("f", "json")
121
- const res = await fetch(url, { signal: AbortSignal.timeout(30_000) })
122
132
 
123
- if (!res.ok) throw new Error(`Failed to discover NAD record count: HTTP ${res.status}`)
124
- const data = (await res.json()) as { count?: number }
133
+ const data = await nadClient
134
+ .fetch<{ count?: number }>({ url: url.toString(), timeout: 30_000 })
135
+ .then(pluckResponseData)
125
136
 
126
137
  if (typeof data.count !== "number") throw new Error("NAD count query returned no count field")
127
138
 
@@ -28,6 +28,7 @@ import { existsSync, mkdirSync, statSync } from "node:fs"
28
28
  import { rm } from "node:fs/promises"
29
29
  import { basename, join } from "node:path"
30
30
 
31
+ import { APIClient, pluckResponseData } from "@mailwoman/core/api"
31
32
  import { extractZipEntry, listZipEntries } from "@mailwoman/core/fs/zip"
32
33
  import { sha256File } from "@mailwoman/core/utils"
33
34
 
@@ -53,13 +54,14 @@ interface SourceManifest {
53
54
  * `NPPES_Data_Dissemination_<Month>_<Year>*.zip`; weekly files carry a `MMDDYY_MMDDYY` date range, which we exclude.
54
55
  */
55
56
  async function discoverLatestZip(): Promise<string | undefined> {
56
- const res = await fetch(INDEX_URL, {
57
- headers: { "Accept-Encoding": "gzip, br" },
58
- signal: AbortSignal.timeout(60_000),
57
+ // `responseType: "text"` — the index is HTML, scraped by regex below.
58
+ const html = await new APIClient({
59
+ displayName: "nppes-index",
60
+ retry: true,
61
+ axios: { headers: { "Accept-Encoding": "gzip, br" } },
59
62
  })
60
-
61
- if (!res.ok) throw new Error(`HTTP ${res.status} ${res.statusText} on ${INDEX_URL}`)
62
- const html = await res.text()
63
+ .fetch<string>({ url: INDEX_URL, responseType: "text", timeout: 60_000 })
64
+ .then(pluckResponseData)
63
65
 
64
66
  for (const match of html.matchAll(/NPPES_Data_Dissemination_[A-Za-z]+_\d{4}[^"]*\.zip/g)) {
65
67
  const name = match[0]
@@ -62,7 +62,9 @@ import { pipeline } from "node:stream/promises"
62
62
  import { setTimeout as sleep } from "node:timers/promises"
63
63
  import { promisify } from "node:util"
64
64
 
65
+ import { APIClient, isSuccessStatus } from "@mailwoman/core/api"
65
66
  import { $private } from "@mailwoman/core/env"
67
+ import { ByteFormatter } from "@mailwoman/core/fs/utils"
66
68
  import { sha256File } from "@mailwoman/core/utils"
67
69
 
68
70
  import type { BaseFetchOptions, FetchSummary } from "./download.ts"
@@ -82,8 +84,6 @@ const HTTP_OK = 200
82
84
  */
83
85
  const MIN_PLAUSIBLE_SHARD_BYTES = 10_240
84
86
 
85
- const BYTES_PER_KIB = 1024
86
-
87
87
  const execFileAsync = promisify(execFile)
88
88
 
89
89
  const OA_BASE = "https://batch.openaddresses.io"
@@ -132,20 +132,6 @@ async function countLines(path: string): Promise<number> {
132
132
  return count
133
133
  }
134
134
 
135
- function humanBytes(bytes: number): string {
136
- const units = ["B", "KiB", "MiB", "GiB", "TiB"]
137
- let value = bytes
138
- let unit = 0
139
-
140
- while (value >= BYTES_PER_KIB && unit < units.length - 1) {
141
- value /= 1024
142
-
143
- unit++
144
- }
145
-
146
- return `${value.toFixed(unit === 0 ? 0 : 1)}${units[unit]}`
147
- }
148
-
149
135
  interface StreamDownloadOpts {
150
136
  headers?: Record<string, string>
151
137
  timeoutMs: number
@@ -263,18 +249,28 @@ The Canada collection (ca) is ~2 GiB compressed / ~7 GiB uncompressed
263
249
  if (collectionID === undefined) {
264
250
  report?.(`Unknown country code '${country}'. Fetching collection list to find ID...`)
265
251
 
266
- const res = await fetch(`${OA_BASE}/api/collections`, {
267
- headers: { Authorization: `Bearer ${token}`, "Accept-Encoding": "gzip, br" },
268
- signal: AbortSignal.timeout(30_000),
252
+ // The collections API only. The COLLECTION ARCHIVES stay on raw `fetch` — they stream multi-gigabyte
253
+ // bodies straight to disk, where response caching is nonsense and axios would buffer them in memory.
254
+ const res = await new APIClient({
255
+ displayName: "openaddresses-api",
256
+ retry: true,
257
+ axios: { headers: { Authorization: `Bearer ${token}`, "Accept-Encoding": "gzip, br" } },
269
258
  })
259
+ // `validateStatus` keeps a non-2xx as a RESPONSE rather than a throw: the caller reports the status and
260
+ // returns `fail(country)`, a graceful path this must not turn into an exception.
261
+ .fetch<OaCollection[]>({
262
+ url: `${OA_BASE}/api/collections`,
263
+ timeout: 30_000,
264
+ validateStatus: () => true,
265
+ })
270
266
 
271
- if (!res.ok) {
267
+ if (!isSuccessStatus(res.status)) {
272
268
  report?.(`ERROR: GET /api/collections returned HTTP ${res.status}.`)
273
269
 
274
270
  return fail(country)
275
271
  }
276
272
 
277
- const collections = (await res.json()) as OaCollection[]
273
+ const collections = res.data
278
274
  const match = collections.find((item) => item.name === country)
279
275
 
280
276
  if (match?.id === undefined) {
@@ -395,7 +391,7 @@ URL tried: ${OA_BASE}/api/collections/${collectionID}/download
395
391
 
396
392
  await writeManifest(manifestPath, manifest)
397
393
 
398
- report?.(` ✓ ${humanBytes(size)} rows=${rowCount} sha256=${sha}`)
394
+ report?.(` ✓ ${ByteFormatter.formatIEC(size)} rows=${rowCount} sha256=${sha}`)
399
395
  report?.(` MANIFEST written to ${manifestPath}`)
400
396
  report?.(`=== done`)
401
397
  report?.(`Feed to the adapter:`)