@mailwoman/corpus 8.6.0 → 9.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (310) hide show
  1. package/README.md +1 -1
  2. package/out/src/adapter.d.ts +41 -0
  3. package/out/src/adapter.d.ts.map +1 -1
  4. package/out/src/adapter.js +46 -1
  5. package/out/src/adapter.js.map +1 -1
  6. package/out/src/adapters/ban/adapter.d.ts +4 -3
  7. package/out/src/adapters/ban/adapter.d.ts.map +1 -1
  8. package/out/src/adapters/ban/adapter.js +60 -67
  9. package/out/src/adapters/ban/adapter.js.map +1 -1
  10. package/out/src/adapters/ban/street-decompose.d.ts.map +1 -1
  11. package/out/src/adapters/ban/street-decompose.js +18 -25
  12. package/out/src/adapters/ban/street-decompose.js.map +1 -1
  13. package/out/src/adapters/fcc-bdc/adapter.d.ts +0 -6
  14. package/out/src/adapters/fcc-bdc/adapter.d.ts.map +1 -1
  15. package/out/src/adapters/fcc-bdc/adapter.js +2 -24
  16. package/out/src/adapters/fcc-bdc/adapter.js.map +1 -1
  17. package/out/src/adapters/geonames/adapter.d.ts.map +1 -1
  18. package/out/src/adapters/geonames/adapter.js +70 -76
  19. package/out/src/adapters/geonames/adapter.js.map +1 -1
  20. package/out/src/adapters/geonames-postal/adapter.d.ts.map +1 -1
  21. package/out/src/adapters/geonames-postal/adapter.js +44 -49
  22. package/out/src/adapters/geonames-postal/adapter.js.map +1 -1
  23. package/out/src/adapters/gnaf/adapter.d.ts.map +1 -1
  24. package/out/src/adapters/gnaf/adapter.js +4 -7
  25. package/out/src/adapters/gnaf/adapter.js.map +1 -1
  26. package/out/src/adapters/gnaf/assemble.d.ts.map +1 -1
  27. package/out/src/adapters/gnaf/assemble.js +6 -11
  28. package/out/src/adapters/gnaf/assemble.js.map +1 -1
  29. package/out/src/adapters/openaddresses/adapter.d.ts.map +1 -1
  30. package/out/src/adapters/openaddresses/adapter.js +2 -7
  31. package/out/src/adapters/openaddresses/adapter.js.map +1 -1
  32. package/out/src/adapters/overture/adapter.d.ts.map +1 -1
  33. package/out/src/adapters/overture/adapter.js +3 -7
  34. package/out/src/adapters/overture/adapter.js.map +1 -1
  35. package/out/src/adapters/state-hi-schools/adapter.d.ts.map +1 -1
  36. package/out/src/adapters/state-hi-schools/adapter.js +55 -73
  37. package/out/src/adapters/state-hi-schools/adapter.js.map +1 -1
  38. package/out/src/adapters/state-ia-contractors/adapter.d.ts.map +1 -1
  39. package/out/src/adapters/state-ia-contractors/adapter.js +58 -76
  40. package/out/src/adapters/state-ia-contractors/adapter.js.map +1 -1
  41. package/out/src/adapters/state-ny-notaries/adapter.d.ts.map +1 -1
  42. package/out/src/adapters/state-ny-notaries/adapter.js +67 -85
  43. package/out/src/adapters/state-ny-notaries/adapter.js.map +1 -1
  44. package/out/src/adapters/state-tx-notaries/adapter.d.ts.map +1 -1
  45. package/out/src/adapters/state-tx-notaries/adapter.js +69 -87
  46. package/out/src/adapters/state-tx-notaries/adapter.js.map +1 -1
  47. package/out/src/adapters/synth-po-box/adapter.d.ts.map +1 -1
  48. package/out/src/adapters/synth-po-box/adapter.js +7 -15
  49. package/out/src/adapters/synth-po-box/adapter.js.map +1 -1
  50. package/out/src/adapters/tiger/street-decompose.d.ts.map +1 -1
  51. package/out/src/adapters/tiger/street-decompose.js +18 -27
  52. package/out/src/adapters/tiger/street-decompose.js.map +1 -1
  53. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts +3 -3
  54. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts.map +1 -1
  55. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js +62 -80
  56. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js.map +1 -1
  57. package/out/src/adapters/usgov-imls-pls/adapter.d.ts.map +1 -1
  58. package/out/src/adapters/usgov-imls-pls/adapter.js +60 -82
  59. package/out/src/adapters/usgov-imls-pls/adapter.js.map +1 -1
  60. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts.map +1 -1
  61. package/out/src/adapters/usgov-irs-bmf/adapter.js +58 -65
  62. package/out/src/adapters/usgov-irs-bmf/adapter.js.map +1 -1
  63. package/out/src/adapters/usgov-nad/adapter.d.ts.map +1 -1
  64. package/out/src/adapters/usgov-nad/adapter.js +5 -8
  65. package/out/src/adapters/usgov-nad/adapter.js.map +1 -1
  66. package/out/src/adapters/usgov-nppes/adapter.d.ts.map +1 -1
  67. package/out/src/adapters/usgov-nppes/adapter.js +58 -76
  68. package/out/src/adapters/usgov-nppes/adapter.js.map +1 -1
  69. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts.map +1 -1
  70. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js +51 -71
  71. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js.map +1 -1
  72. package/out/src/adapters/wof-admin-jp/adapter.d.ts.map +1 -1
  73. package/out/src/adapters/wof-admin-jp/adapter.js +24 -6
  74. package/out/src/adapters/wof-admin-jp/adapter.js.map +1 -1
  75. package/out/src/build.d.ts.map +1 -1
  76. package/out/src/build.js +2 -1
  77. package/out/src/build.js.map +1 -1
  78. package/out/src/golden.d.ts +5 -0
  79. package/out/src/golden.d.ts.map +1 -1
  80. package/out/src/golden.js +23 -9
  81. package/out/src/golden.js.map +1 -1
  82. package/out/src/parquet-wrapper/reader.d.ts +8 -0
  83. package/out/src/parquet-wrapper/reader.d.ts.map +1 -1
  84. package/out/src/parquet-wrapper/reader.js +16 -0
  85. package/out/src/parquet-wrapper/reader.js.map +1 -1
  86. package/out/src/parquet-wrapper/schema.d.ts +4 -0
  87. package/out/src/parquet-wrapper/schema.d.ts.map +1 -1
  88. package/out/src/parquet-wrapper/schema.js.map +1 -1
  89. package/out/src/parquet.d.ts.map +1 -1
  90. package/out/src/parquet.js +2 -13
  91. package/out/src/parquet.js.map +1 -1
  92. package/out/src/shard-recipes/anchor-absorption.d.ts.map +1 -1
  93. package/out/src/shard-recipes/anchor-absorption.js +2 -1
  94. package/out/src/shard-recipes/anchor-absorption.js.map +1 -1
  95. package/out/src/shard-recipes/country-balanced.d.ts.map +1 -1
  96. package/out/src/shard-recipes/country-balanced.js +68 -60
  97. package/out/src/shard-recipes/country-balanced.js.map +1 -1
  98. package/out/src/shard-recipes/fr-fragment.d.ts.map +1 -1
  99. package/out/src/shard-recipes/fr-fragment.js +8 -5
  100. package/out/src/shard-recipes/fr-fragment.js.map +1 -1
  101. package/out/src/shard-recipes/fr-order.d.ts.map +1 -1
  102. package/out/src/shard-recipes/fr-order.js +14 -58
  103. package/out/src/shard-recipes/fr-order.js.map +1 -1
  104. package/out/src/shard-recipes/german.d.ts.map +1 -1
  105. package/out/src/shard-recipes/german.js +11 -58
  106. package/out/src/shard-recipes/german.js.map +1 -1
  107. package/out/src/shard-recipes/index.d.ts.map +1 -1
  108. package/out/src/shard-recipes/index.js +2 -0
  109. package/out/src/shard-recipes/index.js.map +1 -1
  110. package/out/src/shard-recipes/intersection.d.ts +2 -2
  111. package/out/src/shard-recipes/intersection.d.ts.map +1 -1
  112. package/out/src/shard-recipes/intersection.js +24 -62
  113. package/out/src/shard-recipes/intersection.js.map +1 -1
  114. package/out/src/shard-recipes/locale.d.ts.map +1 -1
  115. package/out/src/shard-recipes/locale.js +14 -16
  116. package/out/src/shard-recipes/locale.js.map +1 -1
  117. package/out/src/shard-recipes/no-fragment.d.ts.map +1 -1
  118. package/out/src/shard-recipes/no-fragment.js +8 -5
  119. package/out/src/shard-recipes/no-fragment.js.map +1 -1
  120. package/out/src/shard-recipes/no-street-led.d.ts.map +1 -1
  121. package/out/src/shard-recipes/no-street-led.js +8 -5
  122. package/out/src/shard-recipes/no-street-led.js.map +1 -1
  123. package/out/src/shard-recipes/po-box-cedex.d.ts +3 -3
  124. package/out/src/shard-recipes/po-box-cedex.d.ts.map +1 -1
  125. package/out/src/shard-recipes/po-box-cedex.js +51 -94
  126. package/out/src/shard-recipes/po-box-cedex.js.map +1 -1
  127. package/out/src/shard-recipes/scaffold.d.ts +59 -9
  128. package/out/src/shard-recipes/scaffold.d.ts.map +1 -1
  129. package/out/src/shard-recipes/scaffold.js +51 -30
  130. package/out/src/shard-recipes/scaffold.js.map +1 -1
  131. package/out/src/shard-recipes/street-affix.d.ts.map +1 -1
  132. package/out/src/shard-recipes/street-affix.js +56 -82
  133. package/out/src/shard-recipes/street-affix.js.map +1 -1
  134. package/out/src/shard-recipes/sub-venue-sources.d.ts +231 -0
  135. package/out/src/shard-recipes/sub-venue-sources.d.ts.map +1 -0
  136. package/out/src/shard-recipes/sub-venue-sources.js +669 -0
  137. package/out/src/shard-recipes/sub-venue-sources.js.map +1 -0
  138. package/out/src/shard-recipes/sub-venue.d.ts +269 -0
  139. package/out/src/shard-recipes/sub-venue.d.ts.map +1 -0
  140. package/out/src/shard-recipes/sub-venue.js +709 -0
  141. package/out/src/shard-recipes/sub-venue.js.map +1 -0
  142. package/out/src/shard-recipes/unit.d.ts +1 -1
  143. package/out/src/shard-recipes/unit.d.ts.map +1 -1
  144. package/out/src/shard-recipes/unit.js +22 -64
  145. package/out/src/shard-recipes/unit.js.map +1 -1
  146. package/out/src/split.d.ts +7 -0
  147. package/out/src/split.d.ts.map +1 -1
  148. package/out/src/split.js +7 -0
  149. package/out/src/split.js.map +1 -1
  150. package/out/src/synthesize.d.ts.map +1 -1
  151. package/out/src/synthesize.js +1 -12
  152. package/out/src/synthesize.js.map +1 -1
  153. package/out/src/tools/align-shard.d.ts.map +1 -1
  154. package/out/src/tools/align-shard.js +3 -7
  155. package/out/src/tools/align-shard.js.map +1 -1
  156. package/out/src/tools/audit.d.ts.map +1 -1
  157. package/out/src/tools/audit.js +4 -4
  158. package/out/src/tools/audit.js.map +1 -1
  159. package/out/src/tools/corpus-stats.d.ts +2 -2
  160. package/out/src/tools/corpus-stats.d.ts.map +1 -1
  161. package/out/src/tools/corpus-stats.js +18 -31
  162. package/out/src/tools/corpus-stats.js.map +1 -1
  163. package/out/src/tools/fetch/download.d.ts.map +1 -1
  164. package/out/src/tools/fetch/download.js +5 -6
  165. package/out/src/tools/fetch/download.js.map +1 -1
  166. package/out/src/tools/fetch/imls-pls.d.ts.map +1 -1
  167. package/out/src/tools/fetch/imls-pls.js +2 -2
  168. package/out/src/tools/fetch/imls-pls.js.map +1 -1
  169. package/out/src/tools/fetch/index.d.ts +14 -1
  170. package/out/src/tools/fetch/index.d.ts.map +1 -1
  171. package/out/src/tools/fetch/index.js +14 -1
  172. package/out/src/tools/fetch/index.js.map +1 -1
  173. package/out/src/tools/fetch/nad.d.ts +1 -1
  174. package/out/src/tools/fetch/nad.js +1 -1
  175. package/out/src/tools/fetch/nppes.d.ts.map +1 -1
  176. package/out/src/tools/fetch/nppes.js +2 -1
  177. package/out/src/tools/fetch/nppes.js.map +1 -1
  178. package/out/src/tools/fetch/openaddresses.d.ts +1 -1
  179. package/out/src/tools/fetch/openaddresses.js +1 -1
  180. package/out/src/tools/fetch/ourairports.d.ts +45 -0
  181. package/out/src/tools/fetch/ourairports.d.ts.map +1 -0
  182. package/out/src/tools/fetch/ourairports.js +123 -0
  183. package/out/src/tools/fetch/ourairports.js.map +1 -0
  184. package/out/src/tools/fetch/wikidata-subvenue.d.ts +171 -0
  185. package/out/src/tools/fetch/wikidata-subvenue.d.ts.map +1 -0
  186. package/out/src/tools/fetch/wikidata-subvenue.js +275 -0
  187. package/out/src/tools/fetch/wikidata-subvenue.js.map +1 -0
  188. package/out/src/tools/golden-expand.d.ts.map +1 -1
  189. package/out/src/tools/golden-expand.js +18 -16
  190. package/out/src/tools/golden-expand.js.map +1 -1
  191. package/out/src/tools/golden-promote.d.ts.map +1 -1
  192. package/out/src/tools/golden-promote.js +12 -6
  193. package/out/src/tools/golden-promote.js.map +1 -1
  194. package/out/src/tools/golden-relabel-street.d.ts +196 -0
  195. package/out/src/tools/golden-relabel-street.d.ts.map +1 -0
  196. package/out/src/tools/golden-relabel-street.js +513 -0
  197. package/out/src/tools/golden-relabel-street.js.map +1 -0
  198. package/out/src/tools/index.d.ts +4 -0
  199. package/out/src/tools/index.d.ts.map +1 -1
  200. package/out/src/tools/index.js +4 -0
  201. package/out/src/tools/index.js.map +1 -1
  202. package/out/src/tools/jsonl-to-parquet.d.ts.map +1 -1
  203. package/out/src/tools/jsonl-to-parquet.js +3 -2
  204. package/out/src/tools/jsonl-to-parquet.js.map +1 -1
  205. package/out/src/tools/lint-shard-vocab.d.ts.map +1 -1
  206. package/out/src/tools/lint-shard-vocab.js +3 -2
  207. package/out/src/tools/lint-shard-vocab.js.map +1 -1
  208. package/out/src/tools/lint-shard.d.ts +3 -3
  209. package/out/src/tools/lint-shard.d.ts.map +1 -1
  210. package/out/src/tools/lint-shard.js +23 -32
  211. package/out/src/tools/lint-shard.js.map +1 -1
  212. package/out/src/tools/overlay-manifest.d.ts.map +1 -1
  213. package/out/src/tools/overlay-manifest.js +2 -1
  214. package/out/src/tools/overlay-manifest.js.map +1 -1
  215. package/out/src/tools/overture-subvenue.d.ts +112 -0
  216. package/out/src/tools/overture-subvenue.d.ts.map +1 -0
  217. package/out/src/tools/overture-subvenue.js +144 -0
  218. package/out/src/tools/overture-subvenue.js.map +1 -0
  219. package/out/src/tools/shard-kryptonite.d.ts +1 -1
  220. package/out/src/tools/shard-kryptonite.d.ts.map +1 -1
  221. package/out/src/tools/shard-kryptonite.js +5 -4
  222. package/out/src/tools/shard-kryptonite.js.map +1 -1
  223. package/out/src/tools/shard-translit.d.ts +6 -3
  224. package/out/src/tools/shard-translit.d.ts.map +1 -1
  225. package/out/src/tools/shard-translit.js +8 -6
  226. package/out/src/tools/shard-translit.js.map +1 -1
  227. package/out/src/tools/sub-venue-lexicon.d.ts +507 -0
  228. package/out/src/tools/sub-venue-lexicon.d.ts.map +1 -0
  229. package/out/src/tools/sub-venue-lexicon.js +817 -0
  230. package/out/src/tools/sub-venue-lexicon.js.map +1 -0
  231. package/out/src/tools/sub-venue-promotions.d.ts +94 -0
  232. package/out/src/tools/sub-venue-promotions.d.ts.map +1 -0
  233. package/out/src/tools/sub-venue-promotions.js +266 -0
  234. package/out/src/tools/sub-venue-promotions.js.map +1 -0
  235. package/out/src/wof-json.d.ts +2 -2
  236. package/out/src/wof-json.d.ts.map +1 -1
  237. package/out/src/wof-json.js +5 -8
  238. package/out/src/wof-json.js.map +1 -1
  239. package/package.json +8 -8
  240. package/src/adapter.ts +58 -1
  241. package/src/adapters/ban/adapter.ts +55 -65
  242. package/src/adapters/ban/street-decompose.ts +17 -25
  243. package/src/adapters/fcc-bdc/adapter.ts +2 -33
  244. package/src/adapters/geonames/adapter.ts +64 -72
  245. package/src/adapters/geonames-postal/adapter.ts +42 -50
  246. package/src/adapters/gnaf/adapter.ts +4 -7
  247. package/src/adapters/gnaf/assemble.ts +6 -10
  248. package/src/adapters/openaddresses/adapter.ts +2 -7
  249. package/src/adapters/overture/adapter.ts +3 -6
  250. package/src/adapters/state-hi-schools/adapter.ts +50 -73
  251. package/src/adapters/state-ia-contractors/adapter.ts +52 -76
  252. package/src/adapters/state-ny-notaries/adapter.ts +61 -85
  253. package/src/adapters/state-tx-notaries/adapter.ts +60 -84
  254. package/src/adapters/synth-po-box/adapter.ts +7 -17
  255. package/src/adapters/tiger/street-decompose.ts +21 -31
  256. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +61 -84
  257. package/src/adapters/usgov-imls-pls/adapter.ts +54 -82
  258. package/src/adapters/usgov-irs-bmf/adapter.ts +53 -63
  259. package/src/adapters/usgov-nad/adapter.ts +5 -8
  260. package/src/adapters/usgov-nppes/adapter.ts +51 -75
  261. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +45 -71
  262. package/src/adapters/wof-admin-jp/adapter.ts +34 -8
  263. package/src/build.ts +2 -1
  264. package/src/golden.ts +24 -9
  265. package/src/parquet-wrapper/reader.ts +19 -0
  266. package/src/parquet-wrapper/schema.ts +4 -0
  267. package/src/parquet.ts +3 -16
  268. package/src/shard-recipes/anchor-absorption.ts +2 -1
  269. package/src/shard-recipes/country-balanced.ts +69 -70
  270. package/src/shard-recipes/fr-fragment.ts +10 -7
  271. package/src/shard-recipes/fr-order.ts +13 -63
  272. package/src/shard-recipes/german.ts +12 -65
  273. package/src/shard-recipes/index.ts +2 -0
  274. package/src/shard-recipes/intersection.ts +32 -60
  275. package/src/shard-recipes/locale.ts +14 -16
  276. package/src/shard-recipes/no-fragment.ts +10 -7
  277. package/src/shard-recipes/no-street-led.ts +10 -7
  278. package/src/shard-recipes/po-box-cedex.ts +68 -119
  279. package/src/shard-recipes/scaffold.ts +82 -28
  280. package/src/shard-recipes/street-affix.ts +58 -95
  281. package/src/shard-recipes/sub-venue-sources.ts +895 -0
  282. package/src/shard-recipes/sub-venue.ts +982 -0
  283. package/src/shard-recipes/unit.ts +22 -70
  284. package/src/split.ts +7 -0
  285. package/src/synthesize.ts +1 -15
  286. package/src/tools/align-shard.ts +3 -6
  287. package/src/tools/audit.ts +6 -5
  288. package/src/tools/corpus-stats.ts +25 -33
  289. package/src/tools/fetch/download.ts +7 -5
  290. package/src/tools/fetch/imls-pls.ts +2 -2
  291. package/src/tools/fetch/index.ts +14 -1
  292. package/src/tools/fetch/nad.ts +1 -1
  293. package/src/tools/fetch/nppes.ts +2 -1
  294. package/src/tools/fetch/openaddresses.ts +1 -1
  295. package/src/tools/fetch/ourairports.ts +166 -0
  296. package/src/tools/fetch/wikidata-subvenue.ts +386 -0
  297. package/src/tools/golden-expand.ts +18 -13
  298. package/src/tools/golden-promote.ts +15 -6
  299. package/src/tools/golden-relabel-street.ts +748 -0
  300. package/src/tools/index.ts +4 -0
  301. package/src/tools/jsonl-to-parquet.ts +3 -2
  302. package/src/tools/lint-shard-vocab.ts +3 -2
  303. package/src/tools/lint-shard.ts +33 -36
  304. package/src/tools/overlay-manifest.ts +2 -1
  305. package/src/tools/overture-subvenue.ts +215 -0
  306. package/src/tools/shard-kryptonite.ts +5 -4
  307. package/src/tools/shard-translit.ts +12 -7
  308. package/src/tools/sub-venue-lexicon.ts +1250 -0
  309. package/src/tools/sub-venue-promotions.ts +330 -0
  310. package/src/wof-json.ts +5 -8
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sub-venue-sources.d.ts","sourceRoot":"","sources":["../../../src/shard-recipes/sub-venue-sources.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,669 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The READ half of the `sub-venue` shard recipe (#35 step 4): which surfaces a locale may emit,
7
+ * what identifier follows them in that region, and which real names become the confound negatives.
8
+ * `sub-venue.ts` owns the WRITE half (line rendering + the emit loop) and the recipe registration;
9
+ * split because the two halves together run past the 750-line file cap, and this is the seam — one
10
+ * side reads disk and the ledger, the other side never touches either.
11
+ *
12
+ * Every rule in this file is a rule about EVIDENCE, and each one has a measurement behind it:
13
+ *
14
+ * - {@link promotedSurfacesFor} — a promotion names a designator, a phrase AND a locale, because the
15
+ * same token is a designator in one language and a disaster in another (`hall` is 0-of-3,273 in
16
+ * Great Britain and 35-of-40 in France).
17
+ * - {@link hasPromotedShape} — an `identifier-required` promotion is exercised only as
18
+ * `<phrase> <identifier>`. The de-DE `halle` board is the founding case: its 168-hit confound
19
+ * includes the CITY Halle (Saale), and only the identifier-bearing shape separates them.
20
+ * - {@link buildIdentifierModel} — the identifier distribution is measured per REGION, because it
21
+ * differs by country far more than the shared vocabulary suggests (GB gates 71% bare digit, ES
22
+ * 35% ranges).
23
+ * - {@link isVenueSlotName} / {@link isSignIdentifier} — the filters that keep bus-stop codes, route
24
+ * descriptions and street names out of the slots they would mislabel. Both were written FROM
25
+ * smoke output, not predicted; the docstrings name the strings that produced them.
26
+ */
27
+ import { readFileSync } from "node:fs";
28
+ import { createRequire } from "node:module";
29
+ import { dirname, resolve } from "node:path";
30
+ import { DatabaseSync } from "node:sqlite";
31
+ import { parseJSONStrict } from "@mailwoman/core/objects";
32
+ import { dataRootPath } from "@mailwoman/core/utils";
33
+ import { classifyIdentifier, readSubVenueJSONL } from "../tools/sub-venue-lexicon.js";
34
+ import { SUBVENUE_PROMOTIONS } from "../tools/sub-venue-promotions.js";
35
+ import { readTuples as readLocaleTuples } from "./locale.js";
36
+ import { makeMulberry32, readTuples as readShardTuples } from "./scaffold.js";
37
+ //#region Lexicon
38
+ const require_ = createRequire(import.meta.url);
39
+ /**
40
+ * The committed lexicon, resolved through the package manifest rather than by counting `..` segments — the same file
41
+ * sits at `corpus/data/` from both the source tree and the compiled `out/` tree, and `createRequire().resolve` finds it
42
+ * from either without a `__isCompiledTree` flag of its own.
43
+ */
44
+ export function defaultLexiconPath() {
45
+ return resolve(dirname(require_.resolve("@mailwoman/corpus/package.json")), "data", "sub-venue-lexicon.json");
46
+ }
47
+ /**
48
+ * Read and parse the lexicon. Strict: a corrupt lexicon is a build failure, not a fallback.
49
+ */
50
+ export function readSubVenueLexicon(path = defaultLexiconPath()) {
51
+ return parseJSONStrict(readFileSync(path, "utf8"));
52
+ }
53
+ //#endregion
54
+ //#region Name filters
55
+ /**
56
+ * Longest venue name kept for the venue slot. The extracts carry a tail of route descriptions and junction names
57
+ * ("Furnival Gate/Moorhead MH2", "SPEKE HALL ROAD/HILLFOOT AVE") that are not venue names at all; a length cap plus
58
+ * {@link isCleanName} removes the bulk of them without a hand list.
59
+ */
60
+ const MAX_VENUE_NAME_LENGTH = 44;
61
+ /**
62
+ * Shortest kept name. Below four characters a "name" is an airport code or a platform letter, not something that can
63
+ * stand in a venue slot.
64
+ */
65
+ const MIN_NAME_LENGTH = 4;
66
+ /**
67
+ * Longest attested sub-venue string, in whitespace tokens. `Terminal 1 Flugsteig B` is four and real; anything longer
68
+ * is a venue's own name that happens to contain a designator.
69
+ */
70
+ const MAX_ATTESTED_TOKENS = 4;
71
+ /**
72
+ * Reject a name that is a route description, a junction, or a code rather than a name: embedded `/`, `;`, `,`, `:`,
73
+ * parentheses, no letters, or a bare source code. Measured motivation, not taste — the GB extract's `platform` tier
74
+ * contributes 7,549 `other`-shaped names like `kntgwdgj` and `SPEKE HALL ROAD/HILLFOOT AVE`.
75
+ */
76
+ export function isCleanName(name) {
77
+ if (name.length < MIN_NAME_LENGTH || name.length > MAX_VENUE_NAME_LENGTH)
78
+ return false;
79
+ // A colon in a name is a qualifier, not part of it — "Porte 4 : Ferrys", "Derby College: Ilkeston Campus".
80
+ if (/[/;,:()[\]<>|]/.test(name))
81
+ return false;
82
+ if (!/\p{L}/u.test(name))
83
+ return false;
84
+ // A name that is all lower-case with no space is a source code (`kntgwdgj`), not a name.
85
+ if (!name.includes(" ") && name === name.toLowerCase())
86
+ return false;
87
+ return true;
88
+ }
89
+ /**
90
+ * Head words that make a "name" something other than a venue name, checked on the FIRST token.
91
+ *
92
+ * Two populations, both found by reading the 2026-08-05 smoke output rather than predicted:
93
+ *
94
+ * - **Street types.** A bus stop is routinely named after the street it stands on, so the FR extract offers `Rue de la
95
+ * Porte Bergault` as a `porte` confound. It IS a confound, but it is a STREET, and putting it in the venue slot would
96
+ * train `Rue …` as a venue name — trading one mislabel for another.
97
+ * - **Stop qualifiers.** British stop names carry a position prefix (`OPPOSITE BRICKLEHAMPTON HALL`, `ADJ THE GREEN`)
98
+ * that names a relationship rather than a place.
99
+ *
100
+ * Per-language and short on purpose: this is a head-token filter over four Latin languages, not a street-type
101
+ * gazetteer. `@mailwoman/corpus` cannot reach the shipped street-type lexicon (it lives behind the gazetteer build),
102
+ * and a longer list here would be a second, drifting copy of it.
103
+ */
104
+ const NON_VENUE_HEAD_WORDS = new Set([
105
+ // en
106
+ "street",
107
+ "road",
108
+ "lane",
109
+ "avenue",
110
+ "drive",
111
+ "way",
112
+ "close",
113
+ "opposite",
114
+ "opp",
115
+ "adj",
116
+ "adjacent",
117
+ "outside",
118
+ "near",
119
+ "nr",
120
+ "stop",
121
+ // fr
122
+ "rue",
123
+ "boulevard",
124
+ "chemin",
125
+ "impasse",
126
+ "allée",
127
+ "allee",
128
+ "route",
129
+ "quai",
130
+ "place",
131
+ // es / ca
132
+ "calle",
133
+ "carrer",
134
+ "avenida",
135
+ "avinguda",
136
+ "carretera",
137
+ "plaza",
138
+ "paseo",
139
+ "camino",
140
+ // de
141
+ "straße",
142
+ "strasse",
143
+ "weg",
144
+ "platz",
145
+ "gasse",
146
+ ]);
147
+ /**
148
+ * Street types that appear at the END of an anglophone street name, checked on the LAST token.
149
+ *
150
+ * The head-word filter cannot see these — English streets are `<name> <type>`, so `Strawberry Hall Lane` and `Guinea
151
+ * Hall Mews` reached the venue slot in the second smoke and would have trained a STREET as a venue name. The
152
+ * street-side confound already has its own class (`designator-street`), drawn from real (street, locality, postcode)
153
+ * pairings, so nothing is lost by refusing these here. German is suffix-compounded rather than suffix-worded and is
154
+ * handled by {@link GERMAN_STREET_TAIL} instead.
155
+ */
156
+ const STREET_TAIL_WORDS = new Set([
157
+ "street",
158
+ "road",
159
+ "lane",
160
+ "avenue",
161
+ "drive",
162
+ "close",
163
+ "crescent",
164
+ "mews",
165
+ "terrace",
166
+ "walk",
167
+ "way",
168
+ "court",
169
+ "gardens",
170
+ "grove",
171
+ "rise",
172
+ "row",
173
+ "parade",
174
+ "esplanade",
175
+ ]);
176
+ const GERMAN_STREET_TAIL = /(?:straße|strasse|weg|platz|gasse|allee|ring|damm)$/;
177
+ /**
178
+ * Is this name usable as a VENUE-slot string (positive venue or negative confound venue)?
179
+ */
180
+ export function isVenueSlotName(name) {
181
+ if (!isCleanName(name))
182
+ return false;
183
+ const words = name.toLowerCase().match(/[\p{L}]+/gu) ?? [];
184
+ if (!words.length)
185
+ return false;
186
+ if (NON_VENUE_HEAD_WORDS.has(words[0]))
187
+ return false;
188
+ const tail = words.at(-1);
189
+ return !STREET_TAIL_WORDS.has(tail) && !GERMAN_STREET_TAIL.test(tail);
190
+ }
191
+ /**
192
+ * Title-case a designator phrase for rendering (`flugsteig` → `Flugsteig`). Single-token by construction: every
193
+ * promoted phrase in the ledger is one word.
194
+ */
195
+ export function titleCase(phrase) {
196
+ return phrase.charAt(0).toUpperCase() + phrase.slice(1);
197
+ }
198
+ /**
199
+ * Word-boundary containment, script-aware enough for the Latin legs this recipe runs.
200
+ *
201
+ * The boundary matters: without it `gate` matches Briggate and `wing` matches Wingate, which is how a harvest teaches
202
+ * itself that a Yorkshire street is a sub-venue. (The lexicon's own harvest carries the same rule, and it drops the
203
+ * boundary only for Han/Kana, which has no word boundaries and no leg here.)
204
+ */
205
+ export function containsPhrase(lowerName, phrase) {
206
+ let from = 0;
207
+ for (;;) {
208
+ const at = lowerName.indexOf(phrase, from);
209
+ if (at === -1)
210
+ return false;
211
+ const before = at === 0 ? "" : lowerName[at - 1];
212
+ const after = lowerName[at + phrase.length] ?? "";
213
+ if (!/[\p{L}\p{N}]/u.test(before) && !/[\p{L}\p{N}]/u.test(after))
214
+ return true;
215
+ from = at + 1;
216
+ }
217
+ }
218
+ /**
219
+ * Identifier-shape classes a sub-venue string may sample.
220
+ *
221
+ * `other` is excluded: it is the junk bucket the lexicon's own examples advertise — `C15/C15A+C15B`, `Segelflug Start
222
+ * 06`, `152, 240`, `de.05374048.drabenderhoehezeithstrasse`. Everything else is a real identifier register.
223
+ */
224
+ const USABLE_IDENTIFIER_SHAPES = new Set([
225
+ "digit",
226
+ "letter",
227
+ "letter-digit",
228
+ "digit-letter",
229
+ "range",
230
+ ]);
231
+ /**
232
+ * One atom of a sign identifier: a short number (`5`, `205`), a single letter (`B`), a letter-then-number (`A12`,
233
+ * `B05`), or a number-then-letter (`2F`, `4S`).
234
+ *
235
+ * The **single** leading letter is the load-bearing part, and it is what the third smoke found. A letter-digit ref with
236
+ * a MULTI-letter prefix is not an identifier read off a sign, it is a network code: the lexicon's own examples of that
237
+ * shape are `BS04`, `BS07`, `PWP2`, `WSW3687`, `RQ8` — campus and platform codes — and the GB extract offers `Arundel
238
+ * Gate AG1` … `AG124`, fourteen bus stops on a Sheffield STREET called Arundel Gate whose stop codes begin with its
239
+ * initials. Admitting two-letter prefixes put all fourteen in the attested pool as `unit`.
240
+ */
241
+ const SIGN_IDENTIFIER_ATOM = /^(?:[0-9]{1,3}|[A-Za-z]|[A-Za-z][0-9]{1,3}|[0-9]{1,3}[A-Za-z]{1,2})$/;
242
+ /**
243
+ * Is `value` what a sub-venue identifier looks like on a sign — an atom, or a range of two?
244
+ *
245
+ * Applied on top of {@link USABLE_IDENTIFIER_SHAPES}, which classifies but does not bound: `WSW3687` classifies as
246
+ * `letter-digit` and is a station code.
247
+ */
248
+ export function isSignIdentifier(value) {
249
+ const parts = value.split(/[/-]/);
250
+ if (parts.length > 2 || parts.some((p) => !p))
251
+ return false;
252
+ return parts.every((part) => SIGN_IDENTIFIER_ATOM.test(part));
253
+ }
254
+ /**
255
+ * Does a real extract name exercise `promoted` in one of the two shapes this shard teaches?
256
+ *
257
+ * Only `<phrase> <identifier>` and (English legs) `<modifier> <phrase>` qualify. That is stricter than "contains the
258
+ * phrase", and the 2026-08-05 smoke is why: the loose test put `Glasgow Clyde College - Langside Campus`, `Terminal de
259
+ * Ferry de Bilbao` and — worst — `Halle Wohnstadt Nord` into the attested pool, the last of which is a bare German
260
+ * `Halle` wearing a name where the ledger requires an identifier. A whole venue's name is not a sub-venue string, and
261
+ * an attested string that violates the promotion's own shape constraint is not attestation of it.
262
+ *
263
+ * The follower is checked with {@link classifyIdentifier} plus {@link isSignIdentifier} rather than "any following
264
+ * word": `Wohnstadt` is a word, `8` is an identifier, and the de-DE board turns on exactly that difference.
265
+ */
266
+ export function hasPromotedShape(lowerName, promoted, modifiers = [], english = false) {
267
+ if (!containsPhrase(lowerName, promoted.phrase))
268
+ return false;
269
+ const escaped = promoted.phrase.replaceAll(/[.*+?^${}()|[\]\\]/g, String.raw `\$&`);
270
+ const withFollower = new RegExp(`(?:^|[^\\p{L}\\p{N}])${escaped}\\s+(\\S+)`, "u");
271
+ const follower = withFollower.exec(lowerName)?.[1];
272
+ if (follower && USABLE_IDENTIFIER_SHAPES.has(classifyIdentifier(follower)) && isSignIdentifier(follower))
273
+ return true;
274
+ if (promoted.identifierRequired)
275
+ return false;
276
+ if (!english || !promoted.modifierEligible)
277
+ return false;
278
+ return modifiers.some((modifier) => new RegExp(`(?:^|[^\\p{L}\\p{N}])${modifier}\\s+${escaped}(?![\\p{L}\\p{N}])`, "u").test(lowerName));
279
+ }
280
+ /**
281
+ * {@link hasPromotedShape} plus the length cap that makes a string usable AS a sub-venue span.
282
+ *
283
+ * The two are separate because the difference decides which POOL a name lands in. `navette n2 vers terminal 2g` carries
284
+ * the promoted shape and is five tokens: too long to be a sub-venue string, and disqualified from being a NEGATIVE
285
+ * precisely because it does contain the shape. It belongs to neither pool, and only splitting the test says so.
286
+ */
287
+ export function matchesPromotedShape(lowerName, promoted, modifiers = [], english = false) {
288
+ if ((lowerName.match(/\S+/g) ?? []).length > MAX_ATTESTED_TOKENS)
289
+ return false;
290
+ return hasPromotedShape(lowerName, promoted, modifiers, english);
291
+ }
292
+ /**
293
+ * The surfaces a locale may emit as `unit`.
294
+ *
295
+ * Two inputs, and the difference between them is the advisory/binding split the ledger's docstring names:
296
+ *
297
+ * - The SHIPPED English vocabulary (`neural/venue-structure.ts`, re-declared in the lexicon as `shipped: true`) is a flat
298
+ * English list with no locale gate. It is promoted-by-shipping for the English legs, because the span proposer fires
299
+ * on it there today and the eval board's target cases are drawn from it.
300
+ * - {@link SUBVENUE_PROMOTIONS} adds the localized surfaces and SUBTRACTS the rejections. A rejection of a shipped
301
+ * designator cannot un-ship it (nothing here stops the proposer firing on "Red Wing"), but it absolutely stops this
302
+ * recipe generating a positive: en-US `wing` produces negatives instead.
303
+ */
304
+ export function promotedSurfacesFor(locale, lexicon, promotions = SUBVENUE_PROMOTIONS) {
305
+ const language = locale.split("-")[0];
306
+ const rejected = new Set(promotions.filter((p) => p.decision === "reject" && p.locale === locale).map((p) => `${p.designatorID}|${p.phrase}`));
307
+ const out = new Map();
308
+ if (language === "en") {
309
+ for (const designator of lexicon.designators) {
310
+ if (!designator.shipped)
311
+ continue;
312
+ const key = `${designator.id}|${designator.id}`;
313
+ if (rejected.has(key))
314
+ continue;
315
+ out.set(key, {
316
+ designatorID: designator.id,
317
+ phrase: designator.id,
318
+ surface: titleCase(designator.id),
319
+ identifierRequired: false,
320
+ modifierEligible: designator.modifierEligible,
321
+ });
322
+ }
323
+ }
324
+ for (const promotion of promotions) {
325
+ if (promotion.decision !== "promote" || promotion.locale !== locale)
326
+ continue;
327
+ const designator = lexicon.designators.find((d) => d.id === promotion.designatorID);
328
+ const key = `${promotion.designatorID}|${promotion.phrase}`;
329
+ if (rejected.has(key))
330
+ continue;
331
+ out.set(key, {
332
+ designatorID: promotion.designatorID,
333
+ phrase: promotion.phrase,
334
+ surface: titleCase(promotion.phrase),
335
+ identifierRequired: promotion.shape === "identifier-required",
336
+ // A promotion marks a SURFACE usable; it does not widen the modifier grammar (the ledger's
337
+ // own words). Modifier eligibility stays the designator's, and only English legs read it.
338
+ modifierEligible: Boolean(designator?.modifierEligible) && promotion.shape !== "identifier-required",
339
+ });
340
+ }
341
+ return [...out.values()].toSorted((a, b) => a.phrase.localeCompare(b.phrase));
342
+ }
343
+ /**
344
+ * The phrases REJECTED in a locale — the negatives' vocabulary.
345
+ */
346
+ export function rejectedPhrasesFor(locale, promotions = SUBVENUE_PROMOTIONS) {
347
+ return promotions.filter((p) => p.decision === "reject" && p.locale === locale).map((p) => p.phrase);
348
+ }
349
+ /**
350
+ * Designators whose refs the pooled fallback is built from.
351
+ *
352
+ * `platform` and `station` are excluded deliberately even though they are by far the largest buckets (GB alone has
353
+ * 25,109 platform digits): a platform ref is a network identifier, and its `other` bucket is 7,549 rows of
354
+ * `kntgwdgj`-style source codes. The three kept here are the ones whose refs are what a person reads off a sign.
355
+ */
356
+ const POOLED_IDENTIFIER_DESIGNATORS = ["gate", "terminal", "campus"];
357
+ /**
358
+ * Minimum usable observations before a (region, designator) uses its OWN identifier distribution.
359
+ *
360
+ * Below this the sample is noise — ES `terminal` has 5 usable refs — so the leg falls back to the region's pooled
361
+ * gate+terminal+campus distribution, which is what the lexicon measured at volume (452–655 refs per region). The
362
+ * fallback keeps the axis that matters (the REGION) and drops only the per-designator refinement.
363
+ */
364
+ const MIN_OWN_SHAPE_OBSERVATIONS = 20;
365
+ /**
366
+ * Build the per-region identifier model out of the lexicon's `identifierShapes`.
367
+ */
368
+ export function buildIdentifierModel(lexicon, region) {
369
+ const byDesignator = new Map();
370
+ const pooled = [];
371
+ for (const row of lexicon.identifierShapes) {
372
+ if (row.region !== region)
373
+ continue;
374
+ if (!USABLE_IDENTIFIER_SHAPES.has(row.shape))
375
+ continue;
376
+ const examples = row.examples.filter((e) => isSignIdentifier(e));
377
+ if (!examples.length)
378
+ continue;
379
+ const bucket = { shape: row.shape, observations: row.observations, examples };
380
+ const list = byDesignator.get(row.designatorID);
381
+ if (list) {
382
+ list.push(bucket);
383
+ }
384
+ else {
385
+ byDesignator.set(row.designatorID, [bucket]);
386
+ }
387
+ if (POOLED_IDENTIFIER_DESIGNATORS.includes(row.designatorID)) {
388
+ pooled.push(bucket);
389
+ }
390
+ }
391
+ return { byDesignator, pooled };
392
+ }
393
+ /**
394
+ * Draw one identifier for `designatorID` in this region.
395
+ *
396
+ * Own distribution when it has {@link MIN_OWN_SHAPE_OBSERVATIONS} usable observations, else the region's pooled one.
397
+ * Shapes are weighted by observation count and an example is drawn uniformly inside the chosen shape — the lexicon
398
+ * ships up to eight per shape, which is the resolution available.
399
+ */
400
+ export function sampleIdentifier(model, designatorID, random) {
401
+ const own = model.byDesignator.get(designatorID) ?? [];
402
+ const ownTotal = own.reduce((sum, b) => sum + b.observations, 0);
403
+ const buckets = ownTotal >= MIN_OWN_SHAPE_OBSERVATIONS ? own : model.pooled;
404
+ if (!buckets.length)
405
+ return null;
406
+ const total = buckets.reduce((sum, b) => sum + b.observations, 0);
407
+ let r = random() * total;
408
+ for (const bucket of buckets) {
409
+ r -= bucket.observations;
410
+ if (r < 0)
411
+ return bucket.examples[Math.floor(random() * bucket.examples.length)];
412
+ }
413
+ const last = buckets.at(-1);
414
+ return last.examples[Math.floor(random() * last.examples.length)];
415
+ }
416
+ /**
417
+ * The pools a source that has nothing to say contributes — en-US has no OSM extract, and DE/ES/GB are outside poi.db's
418
+ * four countries. Empty rather than absent so a leg's merge is unconditional.
419
+ */
420
+ export const EMPTY_NAME_POOLS = {
421
+ venues: [],
422
+ attested: [],
423
+ rejectedVenues: [],
424
+ longerNames: [],
425
+ unpromotedShapes: [],
426
+ };
427
+ /**
428
+ * Is this name a designator sitting inside a longer proper name — the "Grand Central Terminal" class the span
429
+ * proposer's second structural guard already knows about, and which the corpus has to agree with?
430
+ */
431
+ function isLongerProperName(low, name, designatorPhrases) {
432
+ return designatorPhrases.some((phrase) => containsPhrase(low, phrase) && !low.startsWith(phrase) && !/\d/.test(name));
433
+ }
434
+ /**
435
+ * Read one OSM extract and split it into name pools.
436
+ */
437
+ export function readExtractPools(path, query) {
438
+ const rows = readSubVenueJSONL(path);
439
+ const venues = new Set();
440
+ const attested = new Set();
441
+ const rejectedVenues = new Set();
442
+ const longerNames = new Set();
443
+ const unpromotedShapes = new Set();
444
+ for (const row of rows) {
445
+ const name = (row.name ?? "").trim();
446
+ if (!name || !isCleanName(name))
447
+ continue;
448
+ const low = name.toLowerCase();
449
+ const venueSlot = isVenueSlotName(name);
450
+ if (row.tier === "venue" && venueSlot) {
451
+ venues.add(name);
452
+ }
453
+ for (const promoted of query.promoted) {
454
+ if (!containsPhrase(low, promoted.phrase))
455
+ continue;
456
+ if (!hasPromotedShape(low, promoted, query.modifiers, query.english)) {
457
+ if (venueSlot) {
458
+ unpromotedShapes.add(name);
459
+ }
460
+ }
461
+ else if (matchesPromotedShape(low, promoted, query.modifiers, query.english)) {
462
+ attested.add(name);
463
+ }
464
+ }
465
+ if (!venueSlot)
466
+ continue;
467
+ if (query.rejectedPhrases.some((phrase) => containsPhrase(low, phrase))) {
468
+ rejectedVenues.add(name);
469
+ }
470
+ if (isLongerProperName(low, name, query.designatorPhrases)) {
471
+ longerNames.add(name);
472
+ }
473
+ }
474
+ return {
475
+ venues: [...venues],
476
+ attested: [...attested],
477
+ rejectedVenues: [...rejectedVenues],
478
+ longerNames: [...longerNames],
479
+ unpromotedShapes: [...unpromotedShapes],
480
+ };
481
+ }
482
+ /**
483
+ * Poi.db category ids this recipe reads, by category NAME (ids are assigned per build, so they are resolved at run time
484
+ * out of `poi_category_codes`).
485
+ *
486
+ * The venue set is the transport + institution categories whose rows name a whole venue — exactly what
487
+ * `overture-subvenue.ts` REJECTED as a lexicon source ("4,071 of them are the token `airport` in the aerodrome's own
488
+ * name") and exactly what a venue slot wants. The confound set is that file's rejection list read as a source of
489
+ * negatives: `shoe_store` contributes 708 hits of `wing` because Red Wing sells boots, and that is the row this shard
490
+ * needs to see with `wing` NOT tagged `unit`.
491
+ */
492
+ const POI_VENUE_CATEGORIES = [
493
+ "airport",
494
+ "airport_terminal",
495
+ "train_station",
496
+ "hospital",
497
+ "college_university",
498
+ ];
499
+ const POI_CONFOUND_CATEGORIES = [
500
+ "shoe_store",
501
+ "furniture_store",
502
+ "home_decor_store",
503
+ "town_hall",
504
+ "martial_arts_club",
505
+ "chicken_wings_restaurant",
506
+ "fire_station",
507
+ ];
508
+ /**
509
+ * Read the venue + confound pools for a country out of `poi.db`.
510
+ *
511
+ * Poi.db is FOUR COUNTRIES — US 11,521,612 / CA 794,418 / FR 721,352 / MX 644,316 — so this is reachable for en-US and
512
+ * fr-FR and nothing else, and a zero here is evidence of absence in four countries rather than in the world.
513
+ */
514
+ export function readPOIPools(dbPath, country, query) {
515
+ const db = new DatabaseSync(dbPath, { readOnly: true });
516
+ try {
517
+ const codes = db.prepare("select id, category from poi_category_codes").all();
518
+ const byName = new Map(codes.map((c) => [c.category, c.id]));
519
+ const venueIDs = POI_VENUE_CATEGORIES.map((c) => byName.get(c)).filter((id) => id != null);
520
+ const confoundIDs = POI_CONFOUND_CATEGORIES.map((c) => byName.get(c)).filter((id) => id != null);
521
+ const wanted = [...venueIDs, ...confoundIDs];
522
+ if (!wanted.length)
523
+ throw new Error(`poi.db at ${dbPath} has none of the expected categories`);
524
+ // One filtered full scan (measured 4.3 s over all 13,681,698 rows, 2026-08-05) rather than one
525
+ // query per category: `poi` is `without rowid` on (h3_cell, category_id, …), so a category
526
+ // predicate scans either way and scanning once is the cheaper shape.
527
+ const rows = db
528
+ .prepare(`select name, category_id from poi where country = ? and name is not null and category_id in (${wanted.map(() => "?").join(",")})`)
529
+ .all(country, ...wanted);
530
+ const venueSet = new Set(venueIDs);
531
+ const venues = new Set();
532
+ const rejectedVenues = new Set();
533
+ const longerNames = new Set();
534
+ for (const row of rows) {
535
+ const name = row.name.trim();
536
+ if (!isVenueSlotName(name))
537
+ continue;
538
+ const low = name.toLowerCase();
539
+ if (venueSet.has(row.category_id)) {
540
+ venues.add(name);
541
+ }
542
+ if (query.rejectedPhrases.some((phrase) => containsPhrase(low, phrase))) {
543
+ rejectedVenues.add(name);
544
+ }
545
+ if (isLongerProperName(low, name, query.designatorPhrases)) {
546
+ longerNames.add(name);
547
+ }
548
+ }
549
+ return {
550
+ venues: [...venues],
551
+ attested: [],
552
+ rejectedVenues: [...rejectedVenues],
553
+ longerNames: [...longerNames],
554
+ unpromotedShapes: [],
555
+ };
556
+ }
557
+ finally {
558
+ db.close();
559
+ }
560
+ }
561
+ /**
562
+ * Merge two sources' name pools.
563
+ */
564
+ export function mergeNamePools(a, b) {
565
+ return {
566
+ venues: [...a.venues, ...b.venues],
567
+ attested: [...a.attested, ...b.attested],
568
+ rejectedVenues: [...a.rejectedVenues, ...b.rejectedVenues],
569
+ longerNames: [...a.longerNames, ...b.longerNames],
570
+ unpromotedShapes: [...a.unpromotedShapes, ...b.unpromotedShapes],
571
+ };
572
+ }
573
+ //#endregion
574
+ //#region Address context
575
+ /**
576
+ * DE + ES address context. Both read through {@link readLocaleTuples}, the `locale` recipe's own streaming + reservoir
577
+ * reader, so the CSV handling (quoted fields, CRLF, city-noise cleaning, the DE per-part region fallback) has exactly
578
+ * one implementation.
579
+ *
580
+ * DE reads `europe.zip`'s two members rather than `oa-cache/de__*.zip`: the cached per-state zips the `locale` recipe
581
+ * names are not materialized on this host, and the archive members are byte-identical to OA's current run (verified in
582
+ * `corpus/AGENTS.md`'s "a file's mtime is not its data's vintage" note).
583
+ */
584
+ const CONTEXT_PARTS = {
585
+ DE: [
586
+ { zip: dataRootPath("openaddresses", "europe.zip"), csv: "de/berlin.csv", region: "Berlin" },
587
+ { zip: dataRootPath("openaddresses", "europe.zip"), csv: "de/sn/statewide.csv", region: "Sachsen" },
588
+ ],
589
+ ES: [{ path: dataRootPath("openaddresses", "extracted", "es", "countrywide.csv") }],
590
+ };
591
+ /**
592
+ * Load the address skeletons every leg renders onto: GB / US / FR from the house-venue v3 tuples (the same 176,519 real
593
+ * rows the `synth-house-venue` shard is built from, so the two shards' address halves are drawn from one pool), DE and
594
+ * ES streamed out of OpenAddresses.
595
+ */
596
+ export async function loadContextTuples(tuplesPath, seed) {
597
+ const byCountry = new Map();
598
+ for await (const tuple of readShardTuples(tuplesPath)) {
599
+ const country = String(tuple.country ?? "");
600
+ if (!country || !tuple.locality || !tuple.street)
601
+ continue;
602
+ const mapped = {
603
+ house_number: String(tuple.houseNumber ?? tuple.house_number ?? ""),
604
+ street: String(tuple.street),
605
+ locality: String(tuple.locality),
606
+ region: String(tuple.region ?? ""),
607
+ postcode: String(tuple.postcode ?? ""),
608
+ };
609
+ const list = byCountry.get(country);
610
+ if (list) {
611
+ list.push(mapped);
612
+ }
613
+ else {
614
+ byCountry.set(country, [mapped]);
615
+ }
616
+ }
617
+ for (const [country, parts] of Object.entries(CONTEXT_PARTS)) {
618
+ if (byCountry.has(country))
619
+ continue;
620
+ const pooled = [];
621
+ for (const [index, part] of parts.entries()) {
622
+ // A dedicated stream PRNG, seeded per part, so the input sample is reproducible without
623
+ // perturbing the emit loop's draws (the `locale` recipe's rule, kept).
624
+ const streamRandom = makeMulberry32(seed + index);
625
+ for (const tuple of await readLocaleTuples(part, streamRandom)) {
626
+ pooled.push(tuple);
627
+ }
628
+ }
629
+ byCountry.set(country, pooled);
630
+ }
631
+ return byCountry;
632
+ }
633
+ /**
634
+ * Shortest token that can carry a `-gate` street suffix and still be a NAME rather than the bare word: `gate` itself is
635
+ * four characters, so the class starts at five (`Highgate`, `Moorgate`, `Stonegate`).
636
+ */
637
+ const MIN_GATE_SUFFIX_TOKEN_LENGTH = 5;
638
+ export function buildStreetNegatives(context, designatorPhrases, modifiers, country) {
639
+ const designatorSet = new Set(designatorPhrases);
640
+ const modifierSet = new Set(modifiers);
641
+ const designator = [];
642
+ const modifierDesignator = [];
643
+ const gateSuffix = [];
644
+ for (const tuple of context) {
645
+ const tokens = tuple.street.toLowerCase().match(/[\p{L}]+/gu) ?? [];
646
+ let isDesignator = false;
647
+ let isPair = false;
648
+ for (const [index, token] of tokens.entries()) {
649
+ if (designatorSet.has(token)) {
650
+ isDesignator = true;
651
+ }
652
+ if (index + 1 < tokens.length && modifierSet.has(token) && designatorSet.has(tokens[index + 1])) {
653
+ isPair = true;
654
+ }
655
+ if (country === "GB" && token.length >= MIN_GATE_SUFFIX_TOKEN_LENGTH && token.endsWith("gate")) {
656
+ gateSuffix.push(tuple);
657
+ }
658
+ }
659
+ if (isPair) {
660
+ modifierDesignator.push(tuple);
661
+ }
662
+ else if (isDesignator) {
663
+ designator.push(tuple);
664
+ }
665
+ }
666
+ return { designator, modifierDesignator, gateSuffix };
667
+ }
668
+ //#endregion
669
+ //# sourceMappingURL=sub-venue-sources.js.map