@mailwoman/corpus 10.3.0 → 10.5.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 (361) hide show
  1. package/README.md +17 -17
  2. package/data/PROVENANCE.md +287 -273
  3. package/data/address-source-register.json +545 -23
  4. package/data/license-decisions.json +120 -0
  5. package/data/licenses/PROVENANCE.md +5 -5
  6. package/data/licenses/cdla-permissive-2.0.md +6 -6
  7. package/data/licenses/geonames-publication.md +5 -5
  8. package/data/licenses/licence-ouverte-2.0.md +3 -3
  9. package/data/licenses/national-address-database.md +12 -12
  10. package/data/licenses/nlod-2.0.md +3 -3
  11. package/data/licenses/sirene-publication.md +13 -6
  12. package/data/licenses/whosonfirst-licenses.md +4 -4
  13. package/data/source-resolutions.json +46 -0
  14. package/data/training-manifests/v0.7.2-address-systems.effective.json +1169 -0
  15. package/data/training-manifests/v0.7.2-address-systems.json +550 -0
  16. package/lib/adapters/cordis/adapter.ts +9 -9
  17. package/lib/adapters/gleif/adapter.ts +7 -8
  18. package/lib/adapters/po-box/adapter.ts +1 -1
  19. package/lib/adapters/source-id.ts +1 -1
  20. package/lib/adapters.ts +4 -2
  21. package/lib/au/adapters/gnaf/adapter.ts +1 -1
  22. package/lib/be/adapters/brussels/adapter.ts +3 -3
  23. package/lib/be/adapters/vlaanderen/adapter.ts +1 -1
  24. package/lib/be/adapters/wallonie/adapter.ts +1 -1
  25. package/lib/build/align.ts +1 -1
  26. package/lib/build.ts +1 -1
  27. package/lib/cz/adapters/cuzk/adapter.ts +2 -2
  28. package/lib/cz/tools/fetch/cuzk.ts +6 -6
  29. package/lib/de/recipes/locale.ts +1 -1
  30. package/lib/dk/adapters/inspire/adapter.ts +1 -1
  31. package/lib/ee/tools/fetch/ads.ts +2 -2
  32. package/lib/es/adapters/bizkaia/adapter.ts +1 -1
  33. package/lib/es/adapters/navarra/adapter.ts +2 -2
  34. package/lib/es/cadastre.ts +2 -2
  35. package/lib/es/tools/fetch/bizkaia.ts +8 -8
  36. package/lib/es/tools/fetch/catastro.ts +6 -6
  37. package/lib/es/tools/fetch/navarra.ts +4 -4
  38. package/lib/fingerprints.ts +3 -3
  39. package/lib/fr/adapters/finess/adapter.ts +6 -5
  40. package/lib/fr/adapters/finess/voie-type.ts +1 -1
  41. package/lib/fr/tools/fetch/annuaire-education.ts +3 -3
  42. package/lib/gb/adapters/os-open/adapter.ts +453 -0
  43. package/lib/gb/constituent-country.ts +52 -0
  44. package/lib/gb/os-open/nearest-link.ts +169 -0
  45. package/lib/gb/os-open/open-names.ts +180 -0
  46. package/lib/gb/os-open/open-roads.ts +145 -0
  47. package/lib/gb/tools/fetch/os-open.ts +251 -0
  48. package/lib/inspire/address.ts +3 -3
  49. package/lib/international/recipes/locale.ts +41 -7
  50. package/lib/it/adapters/anac/adapter.ts +4 -3
  51. package/lib/jp/adapters/wof-admin/adapter.ts +2 -2
  52. package/lib/kr/adapters/juso/label-rows.ts +1 -1
  53. package/lib/kr/adapters/localdata/key-index.ts +2 -1
  54. package/lib/lu/adapters/bd-adresses/adapter.ts +1 -1
  55. package/lib/nl/adapters/kadaster/adapter.ts +2 -2
  56. package/lib/nl/tools/fetch/kadaster.ts +2 -2
  57. package/lib/parquet/shuffle.ts +2 -2
  58. package/lib/parquet/span-sql.ts +8 -6
  59. package/lib/parquet/streams.ts +1 -1
  60. package/lib/parquet/writers.ts +2 -2
  61. package/lib/pl/tools/fetch/emuia.ts +2 -2
  62. package/lib/recipes/scaffold.ts +2 -2
  63. package/lib/recipes/sources.ts +3 -3
  64. package/lib/recipes/sub/venue/render.ts +1 -1
  65. package/lib/recipes/sub/venue/sources.ts +4 -3
  66. package/lib/recipes/sub/venue.ts +4 -3
  67. package/lib/recipes/venue.ts +11 -11
  68. package/lib/registers.ts +7 -0
  69. package/lib/ro/adapters/retea-scolara/adapter.ts +2 -2
  70. package/lib/runner.ts +3 -3
  71. package/lib/sk/tools/fetch/inspire.ts +3 -3
  72. package/lib/source-register/effective-manifest.ts +14 -8
  73. package/lib/source-register/training-manifest.ts +30 -2
  74. package/lib/source-register/types.ts +2 -2
  75. package/lib/source-register.ts +1 -0
  76. package/lib/subvenue/harvest.ts +6 -5
  77. package/lib/subvenue/promotions.ts +3 -3
  78. package/lib/subvenue/surfaces.ts +1 -1
  79. package/lib/subvenue/table.ts +6 -6
  80. package/lib/surfaces/locale.ts +1 -1
  81. package/lib/synthesizers/anchor-absorption.ts +1 -1
  82. package/lib/synthesizers/boundary-stress.ts +3 -2
  83. package/lib/synthesizers/no-street.ts +1 -1
  84. package/lib/synthesizers/po-box.ts +1 -1
  85. package/lib/synthesizers/tail.ts +2 -2
  86. package/lib/test-kit/corpus-recipe.ts +1 -1
  87. package/lib/tools/align-canonical.ts +3 -2
  88. package/lib/tools/audit.ts +1 -1
  89. package/lib/tools/corpus-stats.ts +1 -1
  90. package/lib/tools/fetch/download/network.ts +1 -1
  91. package/lib/tools/fetch/geonames/dump.ts +1 -1
  92. package/lib/tools/fetch/gleif.ts +2 -2
  93. package/lib/tools/fetch/header.ts +1 -1
  94. package/lib/tools/fetch/inspire-addresses.ts +1 -1
  95. package/lib/tools/fetch/vlaanderen-ad.ts +1 -1
  96. package/lib/tools/fetch/wfs-harvest.ts +6 -6
  97. package/lib/tools/fetch/zip-archive.ts +2 -2
  98. package/lib/tools/fetch.ts +2 -0
  99. package/lib/tools/golden/expand.ts +1 -1
  100. package/lib/tools/golden/promote.ts +3 -3
  101. package/lib/tools/golden/relabel-street/directory.ts +2 -1
  102. package/lib/tools/lint/recipe-output/vocab.ts +2 -2
  103. package/lib/tools/lint/recipe-output.ts +1 -1
  104. package/lib/tools/migrate/source-names.ts +1 -1
  105. package/lib/tools/overlay/manifest.ts +62 -10
  106. package/lib/tools/overlay/split-slice.ts +2 -10
  107. package/lib/tools/postcode-triples.ts +2 -2
  108. package/lib/tools/sub/venue/head-nouns.ts +4 -3
  109. package/lib/types.ts +3 -3
  110. package/lib/us/adapters/state/tx-notaries/adapter.ts +1 -1
  111. package/lib/us/adapters/usgov/irs-bmf/adapter.ts +2 -2
  112. package/lib/us/adapters/usgov/nad/adapter.ts +1 -1
  113. package/lib/us/adapters/usgov/nppes/adapter.ts +2 -2
  114. package/lib/us/synthesizers/intersection.ts +4 -4
  115. package/lib/us/synthesizers/street.ts +3 -3
  116. package/lib/us/tools/fetch/nad.ts +2 -2
  117. package/lib/utils/split.ts +59 -6
  118. package/lib/utils/wof-json.ts +1 -1
  119. package/out/adapters/cordis/adapter.d.ts +7 -7
  120. package/out/adapters/cordis/adapter.js +9 -9
  121. package/out/adapters/cordis/adapter.js.map +1 -1
  122. package/out/adapters/gleif/adapter.d.ts +7 -8
  123. package/out/adapters/gleif/adapter.d.ts.map +1 -1
  124. package/out/adapters/gleif/adapter.js +7 -8
  125. package/out/adapters/gleif/adapter.js.map +1 -1
  126. package/out/adapters/po-box/adapter.d.ts +1 -1
  127. package/out/adapters/source-id.d.ts +1 -1
  128. package/out/adapters/source-id.js +1 -1
  129. package/out/adapters.d.ts +2 -2
  130. package/out/adapters.d.ts.map +1 -1
  131. package/out/adapters.js +4 -2
  132. package/out/adapters.js.map +1 -1
  133. package/out/au/adapters/gnaf/adapter.js +1 -1
  134. package/out/be/adapters/brussels/adapter.d.ts +2 -2
  135. package/out/be/adapters/brussels/adapter.js +3 -3
  136. package/out/be/adapters/vlaanderen/adapter.d.ts +1 -1
  137. package/out/be/adapters/wallonie/adapter.d.ts +1 -1
  138. package/out/be/adapters/wallonie/adapter.js +1 -1
  139. package/out/build/align.d.ts +1 -1
  140. package/out/build.js +1 -1
  141. package/out/build.js.map +1 -1
  142. package/out/cz/adapters/cuzk/adapter.d.ts +2 -2
  143. package/out/cz/adapters/cuzk/adapter.js +2 -2
  144. package/out/cz/tools/fetch/cuzk.d.ts +2 -2
  145. package/out/cz/tools/fetch/cuzk.js +4 -4
  146. package/out/cz/tools/fetch/cuzk.js.map +1 -1
  147. package/out/de/recipes/locale.d.ts +1 -1
  148. package/out/de/recipes/locale.js +1 -1
  149. package/out/dk/adapters/inspire/adapter.js +1 -1
  150. package/out/ee/tools/fetch/ads.d.ts +2 -2
  151. package/out/ee/tools/fetch/ads.js +1 -1
  152. package/out/es/adapters/bizkaia/adapter.js +1 -1
  153. package/out/es/adapters/navarra/adapter.d.ts +2 -2
  154. package/out/es/adapters/navarra/adapter.js +2 -2
  155. package/out/es/cadastre.d.ts +2 -2
  156. package/out/es/cadastre.js +2 -2
  157. package/out/es/tools/fetch/bizkaia.d.ts +5 -5
  158. package/out/es/tools/fetch/bizkaia.js +6 -6
  159. package/out/es/tools/fetch/bizkaia.js.map +1 -1
  160. package/out/es/tools/fetch/catastro.d.ts +4 -4
  161. package/out/es/tools/fetch/catastro.js +2 -2
  162. package/out/es/tools/fetch/navarra.d.ts +2 -2
  163. package/out/es/tools/fetch/navarra.js +2 -2
  164. package/out/fingerprints.d.ts +3 -3
  165. package/out/fingerprints.js +3 -3
  166. package/out/fr/adapters/finess/adapter.d.ts +6 -5
  167. package/out/fr/adapters/finess/adapter.d.ts.map +1 -1
  168. package/out/fr/adapters/finess/adapter.js +6 -5
  169. package/out/fr/adapters/finess/adapter.js.map +1 -1
  170. package/out/fr/adapters/finess/voie-type.d.ts +1 -1
  171. package/out/fr/adapters/finess/voie-type.js +1 -1
  172. package/out/fr/tools/fetch/annuaire-education.d.ts +3 -3
  173. package/out/fr/tools/fetch/annuaire-education.js +3 -3
  174. package/out/gb/adapters/os-open/adapter.d.ts +165 -0
  175. package/out/gb/adapters/os-open/adapter.d.ts.map +1 -0
  176. package/out/gb/adapters/os-open/adapter.js +298 -0
  177. package/out/gb/adapters/os-open/adapter.js.map +1 -0
  178. package/out/gb/constituent-country.d.ts +36 -0
  179. package/out/gb/constituent-country.d.ts.map +1 -0
  180. package/out/gb/constituent-country.js +30 -0
  181. package/out/gb/constituent-country.js.map +1 -0
  182. package/out/gb/os-open/nearest-link.d.ts +73 -0
  183. package/out/gb/os-open/nearest-link.d.ts.map +1 -0
  184. package/out/gb/os-open/nearest-link.js +133 -0
  185. package/out/gb/os-open/nearest-link.js.map +1 -0
  186. package/out/gb/os-open/open-names.d.ts +92 -0
  187. package/out/gb/os-open/open-names.d.ts.map +1 -0
  188. package/out/gb/os-open/open-names.js +107 -0
  189. package/out/gb/os-open/open-names.js.map +1 -0
  190. package/out/gb/os-open/open-roads.d.ts +80 -0
  191. package/out/gb/os-open/open-roads.d.ts.map +1 -0
  192. package/out/gb/os-open/open-roads.js +79 -0
  193. package/out/gb/os-open/open-roads.js.map +1 -0
  194. package/out/gb/tools/fetch/os-open.d.ts +91 -0
  195. package/out/gb/tools/fetch/os-open.d.ts.map +1 -0
  196. package/out/gb/tools/fetch/os-open.js +166 -0
  197. package/out/gb/tools/fetch/os-open.js.map +1 -0
  198. package/out/inspire/address.d.ts +3 -3
  199. package/out/inspire/address.js +3 -3
  200. package/out/international/recipes/locale.d.ts +6 -0
  201. package/out/international/recipes/locale.d.ts.map +1 -1
  202. package/out/international/recipes/locale.js +29 -7
  203. package/out/international/recipes/locale.js.map +1 -1
  204. package/out/it/adapters/anac/adapter.d.ts +1 -1
  205. package/out/it/adapters/anac/adapter.d.ts.map +1 -1
  206. package/out/it/adapters/anac/adapter.js +4 -3
  207. package/out/it/adapters/anac/adapter.js.map +1 -1
  208. package/out/jp/adapters/wof-admin/adapter.d.ts +2 -2
  209. package/out/jp/adapters/wof-admin/adapter.js +2 -2
  210. package/out/kr/adapters/juso/label-rows.d.ts +1 -1
  211. package/out/kr/adapters/localdata/key-index.d.ts +2 -1
  212. package/out/kr/adapters/localdata/key-index.d.ts.map +1 -1
  213. package/out/kr/adapters/localdata/key-index.js +2 -1
  214. package/out/kr/adapters/localdata/key-index.js.map +1 -1
  215. package/out/lu/adapters/bd-adresses/adapter.d.ts +1 -1
  216. package/out/lu/adapters/bd-adresses/adapter.js +1 -1
  217. package/out/nl/adapters/kadaster/adapter.js +2 -2
  218. package/out/nl/tools/fetch/kadaster.d.ts +2 -2
  219. package/out/nl/tools/fetch/kadaster.js +2 -2
  220. package/out/parquet/shuffle.d.ts +2 -2
  221. package/out/parquet/shuffle.js +2 -2
  222. package/out/parquet/span-sql.d.ts +8 -6
  223. package/out/parquet/span-sql.d.ts.map +1 -1
  224. package/out/parquet/span-sql.js +8 -6
  225. package/out/parquet/span-sql.js.map +1 -1
  226. package/out/parquet/streams.d.ts +1 -1
  227. package/out/parquet/writers.d.ts +2 -2
  228. package/out/parquet/writers.js +1 -1
  229. package/out/pl/tools/fetch/emuia.d.ts +2 -2
  230. package/out/recipes/scaffold.d.ts +2 -2
  231. package/out/recipes/sources.d.ts +2 -2
  232. package/out/recipes/sources.js +3 -3
  233. package/out/recipes/sources.js.map +1 -1
  234. package/out/recipes/sub/venue/render.d.ts +1 -1
  235. package/out/recipes/sub/venue/render.js +1 -1
  236. package/out/recipes/sub/venue/sources.d.ts +4 -3
  237. package/out/recipes/sub/venue/sources.d.ts.map +1 -1
  238. package/out/recipes/sub/venue/sources.js +4 -3
  239. package/out/recipes/sub/venue/sources.js.map +1 -1
  240. package/out/recipes/sub/venue.d.ts +3 -2
  241. package/out/recipes/sub/venue.d.ts.map +1 -1
  242. package/out/recipes/sub/venue.js +2 -2
  243. package/out/recipes/sub/venue.js.map +1 -1
  244. package/out/recipes/venue.d.ts +7 -6
  245. package/out/recipes/venue.d.ts.map +1 -1
  246. package/out/recipes/venue.js +11 -10
  247. package/out/recipes/venue.js.map +1 -1
  248. package/out/registers.d.ts +7 -0
  249. package/out/registers.d.ts.map +1 -1
  250. package/out/registers.js +7 -0
  251. package/out/registers.js.map +1 -1
  252. package/out/ro/adapters/retea-scolara/adapter.d.ts +2 -2
  253. package/out/ro/adapters/retea-scolara/adapter.js +2 -2
  254. package/out/runner.d.ts +3 -3
  255. package/out/runner.js +2 -2
  256. package/out/sk/tools/fetch/inspire.d.ts +3 -3
  257. package/out/sk/tools/fetch/inspire.js +1 -1
  258. package/out/source-register/effective-manifest.d.ts +11 -5
  259. package/out/source-register/effective-manifest.d.ts.map +1 -1
  260. package/out/source-register/effective-manifest.js +9 -4
  261. package/out/source-register/effective-manifest.js.map +1 -1
  262. package/out/source-register/training-manifest.d.ts +27 -0
  263. package/out/source-register/training-manifest.d.ts.map +1 -1
  264. package/out/source-register/training-manifest.js +13 -1
  265. package/out/source-register/training-manifest.js.map +1 -1
  266. package/out/source-register/types.d.ts +2 -2
  267. package/out/source-register.d.ts +1 -1
  268. package/out/source-register.d.ts.map +1 -1
  269. package/out/source-register.js +1 -1
  270. package/out/source-register.js.map +1 -1
  271. package/out/subvenue/harvest.d.ts +4 -3
  272. package/out/subvenue/harvest.d.ts.map +1 -1
  273. package/out/subvenue/harvest.js +6 -5
  274. package/out/subvenue/harvest.js.map +1 -1
  275. package/out/subvenue/promotions.d.ts +2 -2
  276. package/out/subvenue/promotions.js +3 -3
  277. package/out/subvenue/promotions.js.map +1 -1
  278. package/out/subvenue/surfaces.d.ts +1 -1
  279. package/out/subvenue/table.d.ts +6 -6
  280. package/out/subvenue/table.js +1 -1
  281. package/out/surfaces/locale.d.ts +1 -1
  282. package/out/surfaces/locale.js +1 -1
  283. package/out/synthesizers/boundary-stress.d.ts.map +1 -1
  284. package/out/synthesizers/boundary-stress.js +2 -1
  285. package/out/synthesizers/boundary-stress.js.map +1 -1
  286. package/out/synthesizers/tail.d.ts +2 -2
  287. package/out/synthesizers/tail.js +2 -2
  288. package/out/test-kit/corpus-recipe.d.ts +1 -1
  289. package/out/test-kit/corpus-recipe.js +1 -1
  290. package/out/tools/align-canonical.d.ts.map +1 -1
  291. package/out/tools/align-canonical.js +3 -2
  292. package/out/tools/align-canonical.js.map +1 -1
  293. package/out/tools/audit.js +1 -1
  294. package/out/tools/corpus-stats.d.ts +1 -1
  295. package/out/tools/fetch/download/network.d.ts +1 -1
  296. package/out/tools/fetch/download/network.js +1 -1
  297. package/out/tools/fetch/geonames/dump.d.ts +1 -1
  298. package/out/tools/fetch/geonames/dump.js +1 -1
  299. package/out/tools/fetch/gleif.d.ts +2 -2
  300. package/out/tools/fetch/gleif.js +1 -1
  301. package/out/tools/fetch/header.d.ts +1 -1
  302. package/out/tools/fetch/header.js +1 -1
  303. package/out/tools/fetch/inspire-addresses.d.ts +1 -1
  304. package/out/tools/fetch/vlaanderen-ad.d.ts +1 -1
  305. package/out/tools/fetch/wfs-harvest.d.ts +3 -3
  306. package/out/tools/fetch/wfs-harvest.js +4 -4
  307. package/out/tools/fetch/wfs-harvest.js.map +1 -1
  308. package/out/tools/fetch/zip-archive.d.ts +2 -2
  309. package/out/tools/fetch/zip-archive.js +2 -2
  310. package/out/tools/fetch.d.ts +2 -0
  311. package/out/tools/fetch.d.ts.map +1 -1
  312. package/out/tools/fetch.js +2 -0
  313. package/out/tools/fetch.js.map +1 -1
  314. package/out/tools/golden/expand.js +1 -1
  315. package/out/tools/golden/expand.js.map +1 -1
  316. package/out/tools/golden/promote.d.ts +1 -1
  317. package/out/tools/golden/promote.js +3 -3
  318. package/out/tools/golden/promote.js.map +1 -1
  319. package/out/tools/golden/relabel-street/directory.d.ts +2 -1
  320. package/out/tools/golden/relabel-street/directory.d.ts.map +1 -1
  321. package/out/tools/golden/relabel-street/directory.js.map +1 -1
  322. package/out/tools/lint/recipe-output/vocab.js +2 -2
  323. package/out/tools/lint/recipe-output.d.ts +1 -1
  324. package/out/tools/migrate/source-names.d.ts +1 -1
  325. package/out/tools/overlay/manifest.d.ts +15 -2
  326. package/out/tools/overlay/manifest.d.ts.map +1 -1
  327. package/out/tools/overlay/manifest.js +41 -10
  328. package/out/tools/overlay/manifest.js.map +1 -1
  329. package/out/tools/overlay/split-slice.d.ts.map +1 -1
  330. package/out/tools/overlay/split-slice.js +2 -9
  331. package/out/tools/overlay/split-slice.js.map +1 -1
  332. package/out/tools/postcode-triples.d.ts +1 -1
  333. package/out/tools/postcode-triples.js +2 -2
  334. package/out/tools/postcode-triples.js.map +1 -1
  335. package/out/tools/sub/venue/head-nouns.d.ts +1 -1
  336. package/out/tools/sub/venue/head-nouns.d.ts.map +1 -1
  337. package/out/tools/sub/venue/head-nouns.js +4 -3
  338. package/out/tools/sub/venue/head-nouns.js.map +1 -1
  339. package/out/types.d.ts +3 -3
  340. package/out/types.js +1 -1
  341. package/out/us/adapters/state/tx-notaries/adapter.d.ts +1 -1
  342. package/out/us/adapters/state/tx-notaries/adapter.js +1 -1
  343. package/out/us/adapters/usgov/irs-bmf/adapter.d.ts +1 -1
  344. package/out/us/adapters/usgov/irs-bmf/adapter.js +2 -2
  345. package/out/us/adapters/usgov/nad/adapter.d.ts +1 -1
  346. package/out/us/adapters/usgov/nad/adapter.js +1 -1
  347. package/out/us/adapters/usgov/nppes/adapter.d.ts +1 -1
  348. package/out/us/adapters/usgov/nppes/adapter.js +1 -1
  349. package/out/us/synthesizers/intersection.js +4 -4
  350. package/out/us/synthesizers/intersection.js.map +1 -1
  351. package/out/us/synthesizers/street.js +3 -3
  352. package/out/us/synthesizers/street.js.map +1 -1
  353. package/out/us/tools/fetch/nad.d.ts +2 -2
  354. package/out/us/tools/fetch/nad.js +2 -2
  355. package/out/utils/split.d.ts +21 -0
  356. package/out/utils/split.d.ts.map +1 -1
  357. package/out/utils/split.js +49 -5
  358. package/out/utils/split.js.map +1 -1
  359. package/out/utils/wof-json.d.ts +1 -1
  360. package/out/utils/wof-json.js +1 -1
  361. package/package.json +15 -14
@@ -0,0 +1,180 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Reads the road rows of OS Open Names.
7
+ *
8
+ * The CSV product is 819 files, one per 10 km National Grid square, with no header row. OS ships
9
+ * the 34 column names in `Doc/OS_Open_Names_Header.csv`, and the first data row of each file opens
10
+ * with a byte-order mark, which this reader strips from the `ID` value.
11
+ *
12
+ * A road row's `ID` is the roadNameTOID that OS Open Roads links reference, such as
13
+ * `osgb4000000010927776`. Two `LOCAL_TYPE` values are roads: `Named Road`, and
14
+ * `Section Of Named Road` for a road that OS splits where it crosses a postcode district or a
15
+ * settlement.
16
+ */
17
+
18
+ import { CSVSpliterator } from "spliterator"
19
+
20
+ /**
21
+ * The 34 columns of an OS Open Names CSV, in file order, as `Doc/OS_Open_Names_Header.csv` names them.
22
+ */
23
+ export const OPEN_NAMES_COLUMNS = [
24
+ "ID",
25
+ "NAMES_URI",
26
+ "NAME1",
27
+ "NAME1_LANG",
28
+ "NAME2",
29
+ "NAME2_LANG",
30
+ "TYPE",
31
+ "LOCAL_TYPE",
32
+ "GEOMETRY_X",
33
+ "GEOMETRY_Y",
34
+ "MOST_DETAIL_VIEW_RES",
35
+ "LEAST_DETAIL_VIEW_RES",
36
+ "MBR_XMIN",
37
+ "MBR_YMIN",
38
+ "MBR_XMAX",
39
+ "MBR_YMAX",
40
+ "POSTCODE_DISTRICT",
41
+ "POSTCODE_DISTRICT_URI",
42
+ "POPULATED_PLACE",
43
+ "POPULATED_PLACE_URI",
44
+ "POPULATED_PLACE_TYPE",
45
+ "DISTRICT_BOROUGH",
46
+ "DISTRICT_BOROUGH_URI",
47
+ "DISTRICT_BOROUGH_TYPE",
48
+ "COUNTY_UNITARY",
49
+ "COUNTY_UNITARY_URI",
50
+ "COUNTY_UNITARY_TYPE",
51
+ "REGION",
52
+ "REGION_URI",
53
+ "COUNTRY",
54
+ "COUNTRY_URI",
55
+ "RELATED_SPATIAL_OBJECT",
56
+ "SAME_AS_DBPEDIA",
57
+ "SAME_AS_GEONAMES",
58
+ ] as const
59
+
60
+ const column = Object.fromEntries(OPEN_NAMES_COLUMNS.map((name, index) => [name, index])) as Record<
61
+ (typeof OPEN_NAMES_COLUMNS)[number],
62
+ number
63
+ >
64
+
65
+ /**
66
+ * The `LOCAL_TYPE` values that mark a road row.
67
+ */
68
+ export const OPEN_NAMES_ROAD_TYPES: ReadonlySet<string> = new Set(["Named Road", "Section Of Named Road"])
69
+
70
+ /**
71
+ * One road row of OS Open Names, with the columns the corpus adapter reads.
72
+ */
73
+ export interface OpenNamesRoad {
74
+ /**
75
+ * The roadNameTOID, such as `osgb4000000010927776`.
76
+ */
77
+ id: string
78
+ /**
79
+ * The road's name, in the language OS lists first.
80
+ */
81
+ name1: string
82
+ /**
83
+ * The road's other-language name, Welsh or Gaelic, or an empty string.
84
+ */
85
+ name2: string
86
+ /**
87
+ * The outward code of the postcode district holding the road, such as `LS6`.
88
+ */
89
+ postcodeDistrict: string
90
+ /**
91
+ * The settlement holding the road, or an empty string.
92
+ *
93
+ * A bilingual settlement is written `<NAME1> / <NAME2>` of that settlement,
94
+ * such as `Stornoway / Steòrnabhagh`.
95
+ */
96
+ populatedPlace: string
97
+ districtBorough: string
98
+ countyUnitary: string
99
+ }
100
+
101
+ /**
102
+ * Counts from a read, with a reason for each row that did not yield.
103
+ */
104
+ export interface OpenNamesReadStats {
105
+ /**
106
+ * Rows read across every file.
107
+ */
108
+ read: number
109
+ /**
110
+ * Road rows yielded.
111
+ */
112
+ roads: number
113
+ /**
114
+ * Road rows skipped because their `ID` was not among the requested ids.
115
+ */
116
+ unrequested: number
117
+ /**
118
+ * Rows with a column count other than 34.
119
+ */
120
+ malformed: number
121
+ }
122
+
123
+ /**
124
+ * A stats object with every count at zero.
125
+ */
126
+ export function createOpenNamesReadStats(): OpenNamesReadStats {
127
+ return { read: 0, roads: 0, unrequested: 0, malformed: 0 }
128
+ }
129
+
130
+ /**
131
+ * Streams the road rows of one Open Names CSV.
132
+ *
133
+ * @param ids When given, only roads whose `ID` is in the set are yielded.
134
+ */
135
+ export async function* readOpenNamesRoads(
136
+ csvPath: string,
137
+ stats: OpenNamesReadStats,
138
+ ids?: ReadonlySet<string>
139
+ ): AsyncGenerator<OpenNamesRoad> {
140
+ for await (const row of CSVSpliterator.fromAsync<string[]>(csvPath, { header: false })) {
141
+ stats.read++
142
+
143
+ if (row.length !== OPEN_NAMES_COLUMNS.length) {
144
+ stats.malformed++
145
+
146
+ continue
147
+ }
148
+
149
+ if (!OPEN_NAMES_ROAD_TYPES.has(row[column.LOCAL_TYPE]!)) continue
150
+
151
+ const id = row[column.ID]!.replace(/^/u, "")
152
+
153
+ if (ids && !ids.has(id)) {
154
+ stats.unrequested++
155
+
156
+ continue
157
+ }
158
+
159
+ stats.roads++
160
+
161
+ yield {
162
+ id,
163
+ name1: row[column.NAME1]!.trim(),
164
+ name2: row[column.NAME2]!.trim(),
165
+ postcodeDistrict: row[column.POSTCODE_DISTRICT]!.trim(),
166
+ populatedPlace: row[column.POPULATED_PLACE]!.trim(),
167
+ districtBorough: row[column.DISTRICT_BOROUGH]!.trim(),
168
+ countyUnitary: row[column.COUNTY_UNITARY]!.trim(),
169
+ }
170
+ }
171
+ }
172
+
173
+ /**
174
+ * The settlement's first-listed name from a `POPULATED_PLACE` value.
175
+ *
176
+ * OS writes a bilingual settlement as `<NAME1> / <NAME2>`, and NAME1 is the form the adapter emits.
177
+ */
178
+ export function populatedPlaceName1(populatedPlace: string): string {
179
+ return populatedPlace.split(" / ")[0]!.trim()
180
+ }
@@ -0,0 +1,145 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Reads the named road links of an OS Open Roads GML tile.
7
+ *
8
+ * The GML product is one file per 100 km National Grid square. A tile holds `road:RoadLink`,
9
+ * `road:RoadNode` and `road:MotorwayJunction` features. Only the links hold a centreline and a
10
+ * name. A named link has a `road:name1` and a `road:roadNameTOID` whose `xlink:href` is `#` plus
11
+ * the roadNameTOID that OS Open Names uses as its `ID`. A link with only a road number carries a
12
+ * `road:roadNumberTOID` instead, and an unnamed link carries neither.
13
+ *
14
+ * The centreline is a `gml:LineString` in EPSG:27700, written as a `gml:posList` of easting and
15
+ * northing pairs. The reader honors the line's `srsDimension` so a 3D tile would still read its
16
+ * first two ordinates.
17
+ */
18
+
19
+ import { openReadStream } from "@mailwoman/core/fs/streams"
20
+ import { elementAtPath, streamMarkupElements, textAtPath, type MarkupElement } from "@mailwoman/core/html/elements"
21
+ import type { PathBuilderLike } from "path-ts"
22
+
23
+ /**
24
+ * The feature element that carries a centreline.
25
+ */
26
+ export const OPEN_ROADS_LINK_ELEMENT = "road:RoadLink"
27
+
28
+ /**
29
+ * One named road link.
30
+ */
31
+ export interface OpenRoadsLink {
32
+ /**
33
+ * The link's `gml:id`.
34
+ */
35
+ id: string
36
+ /**
37
+ * `road:name1` as the tile spells it.
38
+ */
39
+ name1: string
40
+ /**
41
+ * The roadNameTOID, without the `#` of the reference, such as `osgb4000000010927776`.
42
+ */
43
+ roadNameTOID: string
44
+ /**
45
+ * The centreline as flat `[easting0, northing0, easting1, northing1, …]` in EPSG:27700 meters.
46
+ */
47
+ vertices: Float64Array
48
+ }
49
+
50
+ /**
51
+ * Counts from a read.
52
+ */
53
+ export interface OpenRoadsReadStats {
54
+ /**
55
+ * Road links read.
56
+ */
57
+ links: number
58
+ /**
59
+ * Named links yielded.
60
+ */
61
+ named: number
62
+ /**
63
+ * Links with no `road:name1` or no `road:roadNameTOID`.
64
+ */
65
+ unnamed: number
66
+ /**
67
+ * Named links whose centreline held fewer than two vertices or a non-numeric ordinate.
68
+ */
69
+ malformedGeometry: number
70
+ }
71
+
72
+ /**
73
+ * A stats object with every count at zero.
74
+ */
75
+ export function createOpenRoadsReadStats(): OpenRoadsReadStats {
76
+ return { links: 0, named: 0, unnamed: 0, malformedGeometry: 0 }
77
+ }
78
+
79
+ const LINE_PATH = ["net:centrelineGeometry", "gml:LineString"] as const
80
+
81
+ /**
82
+ * The named link a `road:RoadLink` element describes, or a reason it describes none.
83
+ */
84
+ export function readOpenRoadsLink(element: MarkupElement): OpenRoadsLink | "unnamed" | "malformed-geometry" {
85
+ const name1 = (textAtPath(element, "road:name1") ?? "").trim()
86
+ const href = elementAtPath(element, "road:roadNameTOID")?.attributes["xlink:href"] ?? ""
87
+ const roadNameTOID = href.replace(/^#/u, "").trim()
88
+
89
+ if (!name1 || !roadNameTOID) return "unnamed"
90
+
91
+ const dimension = Number(elementAtPath(element, ...LINE_PATH)?.attributes.srsDimension ?? "2") || 2
92
+ const posList = (textAtPath(element, ...LINE_PATH, "gml:posList") ?? "").trim()
93
+ const ordinates = posList ? posList.split(/\s+/u) : []
94
+ const vertexCount = Math.floor(ordinates.length / dimension)
95
+
96
+ if (vertexCount < 2 || ordinates.length % dimension !== 0) return "malformed-geometry"
97
+
98
+ const vertices = new Float64Array(vertexCount * 2)
99
+
100
+ for (let vertex = 0; vertex < vertexCount; vertex++) {
101
+ const easting = Number(ordinates[vertex * dimension])
102
+ const northing = Number(ordinates[vertex * dimension + 1])
103
+
104
+ if (!Number.isFinite(easting) || !Number.isFinite(northing)) return "malformed-geometry"
105
+
106
+ vertices[vertex * 2] = easting
107
+ vertices[vertex * 2 + 1] = northing
108
+ }
109
+
110
+ return { id: element.attributes["gml:id"] ?? "", name1, roadNameTOID, vertices }
111
+ }
112
+
113
+ /**
114
+ * Streams the named links of one Open Roads GML tile.
115
+ */
116
+ export async function* readOpenRoadsLinks(
117
+ gmlPath: PathBuilderLike,
118
+ stats: OpenRoadsReadStats,
119
+ options: { signal?: AbortSignal } = {}
120
+ ): AsyncGenerator<OpenRoadsLink> {
121
+ for await (const element of streamMarkupElements(openReadStream(gmlPath), OPEN_ROADS_LINK_ELEMENT, {
122
+ xml: true,
123
+ signal: options.signal,
124
+ })) {
125
+ stats.links++
126
+
127
+ const link = readOpenRoadsLink(element)
128
+
129
+ if (link === "unnamed") {
130
+ stats.unnamed++
131
+
132
+ continue
133
+ }
134
+
135
+ if (link === "malformed-geometry") {
136
+ stats.malformedGeometry++
137
+
138
+ continue
139
+ }
140
+
141
+ stats.named++
142
+
143
+ yield link
144
+ }
145
+ }
@@ -0,0 +1,251 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Downloads the three OS OpenData products the `gb-os-open` adapter joins, from the OS Downloads API.
7
+ *
8
+ * The API serves OpenData without a key. `GET /products/<id>/downloads` lists one archive per
9
+ * format and area with its OS-published MD5, and each archive is checked against that MD5 after
10
+ * transfer. A present archive whose MD5 already matches is kept rather than downloaded again.
11
+ *
12
+ * Each archive is unpacked into the subdirectory the adapter reads, `code-point-open/`,
13
+ * `open-names/` or `open-roads/`, so the fetch root is the adapter's `inputPath`. The archive
14
+ * stays beside its members, because its digest is the value the address-source register records.
15
+ */
16
+
17
+ import { APIClient } from "@mailwoman/core/api"
18
+ import { ByteFormatter } from "@mailwoman/core/fs/formatters"
19
+ import { makeDirectories } from "@mailwoman/core/fs/writers"
20
+ import { extractZipEntries } from "@mailwoman/core/fs/zip"
21
+ import { md5File, sha256File } from "@mailwoman/core/hash"
22
+ import { OS_DOWNLOADS_API_BASE, type OSDownload } from "@mailwoman/spatial/gb/os-downloads"
23
+ import { PathBuilder, type PathBuilderLike } from "path-ts"
24
+
25
+ import { GB_OS_OPEN_ADAPTER_ID, GB_OS_OPEN_PRODUCT_DIRECTORIES } from "#gb/adapters/os-open/adapter"
26
+ import type { BaseFetchOptions, FetchSummary, SourceCollectionManifest, SourceManifest } from "#tools/fetch/download"
27
+ import { downloadToFile, writeManifest } from "#tools/fetch/download"
28
+
29
+ /**
30
+ * The license all three products are published under.
31
+ */
32
+ export const GB_OS_OPEN_LICENSE_NAME = "Open Government Licence v3.0"
33
+
34
+ /**
35
+ * The attribution OGL v3 requires, in the wording OS asks for on Code-Point Open,
36
+ * which also carries Royal Mail and ONS data.
37
+ */
38
+ export const GB_OS_OPEN_ATTRIBUTION =
39
+ "Contains OS data © Crown copyright and database right. Contains Royal Mail data © Royal Mail copyright and database right. Contains National Statistics data © Crown copyright and database right."
40
+
41
+ /**
42
+ * One product the adapter reads: its OS Downloads API id, the archive format,
43
+ * and the subdirectory it unpacks into.
44
+ */
45
+ export interface OSOpenProduct {
46
+ productID: string
47
+ format: string
48
+ directory: string
49
+ }
50
+
51
+ /**
52
+ * The three products, in the order they are fetched.
53
+ */
54
+ export const GB_OS_OPEN_PRODUCTS: readonly OSOpenProduct[] = [
55
+ { productID: "CodePointOpen", format: "CSV", directory: GB_OS_OPEN_PRODUCT_DIRECTORIES.codePointOpen },
56
+ { productID: "OpenNames", format: "CSV", directory: GB_OS_OPEN_PRODUCT_DIRECTORIES.openNames },
57
+ { productID: "OpenRoads", format: "GML", directory: GB_OS_OPEN_PRODUCT_DIRECTORIES.openRoads },
58
+ ]
59
+
60
+ /**
61
+ * The manifest entry for one product's archive.
62
+ */
63
+ export interface OSOpenFileManifest extends SourceManifest {
64
+ product_id: string
65
+ format: string
66
+ md5: string
67
+ members: number
68
+ }
69
+
70
+ /**
71
+ * The archive of `product` among the listed downloads, the Great Britain one where an area is stated.
72
+ *
73
+ * @throws Naming the formats offered, when none matches.
74
+ */
75
+ export function selectOSDownload(product: OSOpenProduct, downloads: readonly OSDownload[]): OSDownload {
76
+ const matches = downloads.filter((download) => download.format === product.format)
77
+ const chosen = matches.find((download) => download.area === "GB") ?? (matches.length === 1 ? matches[0] : undefined)
78
+
79
+ if (!chosen) {
80
+ throw new Error(
81
+ `${GB_OS_OPEN_ADAPTER_ID}: OS Downloads API lists no single ${product.format} archive for ${product.productID} ` +
82
+ `(listed: ${downloads.map((download) => `${download.format}/${download.area}`).join(", ") || "nothing"})`
83
+ )
84
+ }
85
+
86
+ return chosen
87
+ }
88
+
89
+ /**
90
+ * The archives the OS Downloads API lists for one product.
91
+ */
92
+ export async function readOSDownloads(
93
+ client: Pick<APIClient, "fetch">,
94
+ product: OSOpenProduct,
95
+ options: { signal?: AbortSignal } = {}
96
+ ): Promise<OSDownload[]> {
97
+ const { data } = await client.fetch<OSDownload[]>({
98
+ method: "GET",
99
+ url: `${OS_DOWNLOADS_API_BASE}/products/${product.productID}/downloads`,
100
+ timeout: 60_000,
101
+ signal: options.signal,
102
+ })
103
+
104
+ if (!Array.isArray(data)) {
105
+ throw new TypeError(
106
+ `${GB_OS_OPEN_ADAPTER_ID}: OS Downloads API answered ${product.productID} with no download list`
107
+ )
108
+ }
109
+
110
+ return data
111
+ }
112
+
113
+ /**
114
+ * Downloads and unpacks one product.
115
+ */
116
+ export async function downloadOSOpenProduct(
117
+ client: Pick<APIClient, "fetch">,
118
+ product: OSOpenProduct,
119
+ options: {
120
+ outputDir: PathBuilderLike
121
+ force?: boolean
122
+ retries?: number
123
+ retryDelayMs?: number
124
+ signal?: AbortSignal
125
+ report?: (line: string) => void
126
+ }
127
+ ): Promise<{ entry: OSOpenFileManifest; downloaded: boolean }> {
128
+ const { report } = options
129
+ const download = selectOSDownload(product, await readOSDownloads(client, product, { signal: options.signal }))
130
+ const productDir = PathBuilder.from(options.outputDir)(product.directory)
131
+ const archivePath = productDir(download.fileName)
132
+
133
+ report?.(
134
+ `=== ${GB_OS_OPEN_ADAPTER_ID} / ${product.productID} ${download.fileName} (${ByteFormatter.formatIEC(download.size)})`
135
+ )
136
+
137
+ await makeDirectories(productDir)
138
+
139
+ const present = options.force ? null : await md5File(archivePath).catch(() => null)
140
+ const downloaded = present !== download.md5
141
+
142
+ if (downloaded) {
143
+ await downloadToFile({
144
+ url: download.url,
145
+ dest: archivePath,
146
+ retries: options.retries,
147
+ retryDelayMs: options.retryDelayMs,
148
+ report,
149
+ })
150
+
151
+ const md5 = await md5File(archivePath)
152
+
153
+ if (md5 !== download.md5) {
154
+ throw new Error(
155
+ `${GB_OS_OPEN_ADAPTER_ID}: ${download.fileName} has md5 ${md5}, and OS publishes ${download.md5} for it`
156
+ )
157
+ }
158
+ } else {
159
+ report?.(` present, and its md5 matches the published ${download.md5}`)
160
+ }
161
+
162
+ const members = await extractZipEntries(archivePath, productDir, { skipExisting: true })
163
+ const sha256 = await sha256File(archivePath)
164
+
165
+ report?.(` ${members.length} members unpacked sha256=${sha256}`)
166
+
167
+ return {
168
+ entry: {
169
+ source_url: download.url,
170
+ filename: download.fileName,
171
+ product_id: product.productID,
172
+ format: product.format,
173
+ bytes: download.size,
174
+ md5: download.md5,
175
+ sha256,
176
+ members: members.length,
177
+ downloaded_at: new Date().toISOString(),
178
+ },
179
+ downloaded,
180
+ }
181
+ }
182
+
183
+ /**
184
+ * Per-invocation options for the registry entry.
185
+ */
186
+ export interface FetchGBOSOpenOptions extends BaseFetchOptions {
187
+ force?: boolean
188
+ retries?: number
189
+ signal?: AbortSignal
190
+ }
191
+
192
+ /**
193
+ * The registry entry: fetches all three products and writes `MANIFEST.json` at the adapter's input root.
194
+ *
195
+ * A product that fails is counted and the others are still taken.
196
+ */
197
+ export async function fetchGBOSOpen(
198
+ options: FetchGBOSOpenOptions,
199
+ report?: (line: string) => void
200
+ ): Promise<FetchSummary> {
201
+ const outputDir = options.outRoot(GB_OS_OPEN_ADAPTER_ID)
202
+ const files: OSOpenFileManifest[] = []
203
+ const failedCodes: string[] = []
204
+
205
+ let fetched = 0
206
+ let skipped = 0
207
+
208
+ await using client = new APIClient({ displayName: GB_OS_OPEN_ADAPTER_ID, minRequestIntervalMs: 250, retry: true })
209
+
210
+ for (const product of GB_OS_OPEN_PRODUCTS) {
211
+ if (options.signal?.aborted) break
212
+
213
+ try {
214
+ const { entry, downloaded } = await downloadOSOpenProduct(client, product, {
215
+ outputDir,
216
+ force: options.force,
217
+ retries: options.retries,
218
+ retryDelayMs: options.retryDelayMs,
219
+ signal: options.signal,
220
+ report,
221
+ })
222
+
223
+ files.push(entry)
224
+
225
+ if (downloaded) {
226
+ fetched += 1
227
+ } else {
228
+ skipped += 1
229
+ }
230
+ } catch (error) {
231
+ report?.(` ✗ ${product.productID}: ${error instanceof Error ? error.message : String(error)}`)
232
+
233
+ failedCodes.push(product.productID)
234
+ }
235
+ }
236
+
237
+ const manifest: SourceCollectionManifest = {
238
+ source: GB_OS_OPEN_ADAPTER_ID,
239
+ source_url: OS_DOWNLOADS_API_BASE,
240
+ license: GB_OS_OPEN_LICENSE_NAME,
241
+ attribution: GB_OS_OPEN_ATTRIBUTION,
242
+ downloaded_at: new Date().toISOString(),
243
+ files,
244
+ }
245
+
246
+ await writeManifest(outputDir("MANIFEST.json"), manifest)
247
+
248
+ report?.(` MANIFEST written to ${outputDir("MANIFEST.json")}`)
249
+
250
+ return { fetched, skipped, failed: failedCodes.length, failedCodes }
251
+ }
@@ -123,7 +123,7 @@ export function codelistValue(uri: string | null | undefined): string | null {
123
123
  /**
124
124
  * Whether a publisher marked this element void.
125
125
  *
126
- * INSPIRE writes `xsi:nil="true"` and usually a `nilReason`.
126
+ * INSPIRE writes `xsi:nil="true"`, in most cases with a `nilReason`.
127
127
  * Brussels writes the nil marker and no reason on all 232,003 of its void `ad:validFrom`
128
128
  * elements, and Czechia writes four `gn:pronunciation` elements nil with no reason.
129
129
  *
@@ -355,7 +355,7 @@ const REFERENCE_ID_PARAMETERS = new Set(["id", "featureid", "resourceid"])
355
355
  * `http://vocab.belgif.be/auth/refnis1995/1000#id` addresses the same term as that URI without it.
356
356
  *
357
357
  * Whether a key joins is not a property of its spelling, so this reads a key
358
- * and the caller decides joinability against the features the publisher actually served.
358
+ * and the caller decides joinability against the features the publisher served.
359
359
  * A check based on the URL's shape once reported 15 of Flanders' 20 references as belonging
360
360
  * to another register, when every one of them addressed a feature of the same service.
361
361
  *
@@ -392,7 +392,7 @@ export function componentJoinKey(href: string): string | null {
392
392
  /**
393
393
  * The spelling a `gn:GeographicalName` holds, reached from a feature that has one.
394
394
  *
395
- * Every name-bearing INSPIRE feature nests the text at
395
+ * Every INSPIRE feature that has a name nests the text at
396
396
  * `…/gn:GeographicalName/gn:spelling/gn:SpellingOfName/gn:text`, under a per-feature
397
397
  * prefix: an `ad:ThoroughfareName` adds `ad:name/ad:ThoroughfareNameValue/ad:name`,
398
398
  * while an `ad:AdminUnitName` and an `ad:AddressAreaName` add only `ad:name`.
@@ -19,6 +19,7 @@ import type { PathBuilderLike } from "path-ts"
19
19
  import { CSVSpliterator } from "spliterator"
20
20
 
21
21
  import { stableSourceID } from "#adapters/source-id"
22
+ import { openConstituentCountryReader } from "#gb/constituent-country"
22
23
  import type { CorpusRecipe } from "#recipes/scaffold"
23
24
  import { defaultRecipeSource } from "#recipes/sources"
24
25
  import { SourceRegister } from "#registers"
@@ -47,6 +48,12 @@ export interface LocalePart {
47
48
  * The street combines `tipo_vial` with `nombre_via`.
48
49
  */
49
50
  cnigRaw?: boolean
51
+ /**
52
+ * Opens a reader that answers the row's region from its postcode, replacing the region column.
53
+ *
54
+ * A GB part answers the constituent country this way, and its county column is not read.
55
+ */
56
+ regionOfPostcode?: () => (postcode: string) => string | null
50
57
  }
51
58
 
52
59
  /**
@@ -158,7 +165,18 @@ const COUNTRY_SOURCES: Record<string, LocaleCountrySource> = {
158
165
  source: defaultRecipeSource("synth-gb"),
159
166
  register: SourceRegister.LandRegistryPricePaid,
160
167
  corpusVersion: "0.9.9",
161
- parts: [{ path: dataRootPath("ppd", "2026-07-22", "gb-tuples.csv"), districtAsLocality: true }],
168
+ // The `REGION` column holds a county, so the constituent country is read from NSUL by postcode instead.
169
+ parts: [
170
+ {
171
+ path: dataRootPath("ppd", "2026-07-22", "gb-tuples.csv"),
172
+ districtAsLocality: true,
173
+ regionOfPostcode: () => {
174
+ const countryOf = openConstituentCountryReader()
175
+
176
+ return (postcode) => countryOf(postcode).name
177
+ },
178
+ },
179
+ ],
162
180
  },
163
181
  }
164
182
 
@@ -224,6 +242,8 @@ export async function readTuples(part: LocalePart, rng: () => number): Promise<L
224
242
  let header: string[] | null = null
225
243
  let seen = 0
226
244
  let dropped = 0
245
+ const regionOf = part.regionOfPostcode?.()
246
+ let regionUnknown = 0
227
247
 
228
248
  try {
229
249
  // The first row holds the column names.
@@ -304,12 +324,24 @@ export async function readTuples(part: LocalePart, rng: () => number): Promise<L
304
324
  continue
305
325
  }
306
326
 
327
+ const postcode = get(cells, cols.post)
328
+
329
+ let region = get(cells, cols.region) || part.region || ""
330
+
331
+ if (regionOf) {
332
+ region = (postcode && regionOf(postcode)) || ""
333
+
334
+ if (!region) {
335
+ regionUnknown++
336
+ }
337
+ }
338
+
307
339
  const tuple: LocaleBaseTuple = {
308
340
  house_number: get(cells, cols.num),
309
341
  street,
310
342
  locality,
311
- region: get(cells, cols.region) || part.region || "",
312
- postcode: get(cells, cols.post),
343
+ region,
344
+ postcode,
313
345
  ...(dependent_locality ? { dependent_locality } : {}),
314
346
  }
315
347
 
@@ -333,6 +365,10 @@ export async function readTuples(part: LocalePart, rng: () => number): Promise<L
333
365
 
334
366
  console.error(` ${part.path ?? part.csv}: ${reservoir.length} sampled of ${seen} rows (${dropped} city-noise drops)`)
335
367
 
368
+ if (regionOf) {
369
+ console.error(` ${part.path ?? part.csv}: ${regionUnknown} rows have no region for their postcode`)
370
+ }
371
+
336
372
  return reservoir
337
373
  }
338
374
 
@@ -425,10 +461,8 @@ export const localeRecipe: CorpusRecipe = {
425
461
  throw new Error(`--country-fraction must be in [0, 1], got ${countryFraction}`)
426
462
  }
427
463
 
428
- // `COUNTRY_SOURCES` and `RECIPE_SOURCES` both use the retired spelling as their key.
429
- // This lookup decides the vocabulary this recipe writes in one place
430
- // rather than in twelve table entries.
431
- const source = opts.sourceName ?? defaultRecipeSource(countrySource.source)
464
+ // Each `COUNTRY_SOURCES` entry already holds the current spelling from `defaultRecipeSource`.
465
+ const source = opts.sourceName ?? countrySource.source
432
466
  const count = opts.count ?? 4000
433
467
  // When unset, each part keeps its own setting.
434
468
  // True also selects the ES pedanía parts.