@mailwoman/corpus 4.16.2 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (262) hide show
  1. package/out/src/adapter.d.ts +23 -26
  2. package/out/src/adapter.d.ts.map +1 -1
  3. package/out/src/adapter.js +25 -28
  4. package/out/src/adapter.js.map +1 -1
  5. package/out/src/adapters/ban/adapter.d.ts.map +1 -1
  6. package/out/src/adapters/ban/adapter.js +10 -10
  7. package/out/src/adapters/ban/adapter.js.map +1 -1
  8. package/out/src/adapters/ban/street-decompose.d.ts +2 -2
  9. package/out/src/adapters/ban/street-decompose.d.ts.map +1 -1
  10. package/out/src/adapters/ban/street-decompose.js +2 -2
  11. package/out/src/adapters/ban/street-decompose.js.map +1 -1
  12. package/out/src/adapters/fcc-bdc/adapter.d.ts +2 -2
  13. package/out/src/adapters/fcc-bdc/adapter.d.ts.map +1 -1
  14. package/out/src/adapters/fcc-bdc/adapter.js +9 -9
  15. package/out/src/adapters/fcc-bdc/adapter.js.map +1 -1
  16. package/out/src/adapters/geonames/adapter.d.ts.map +1 -1
  17. package/out/src/adapters/geonames/adapter.js +5 -5
  18. package/out/src/adapters/geonames/adapter.js.map +1 -1
  19. package/out/src/adapters/geonames-postal/adapter.d.ts.map +1 -1
  20. package/out/src/adapters/geonames-postal/adapter.js +3 -3
  21. package/out/src/adapters/geonames-postal/adapter.js.map +1 -1
  22. package/out/src/adapters/gnaf/adapter.d.ts +3 -3
  23. package/out/src/adapters/gnaf/adapter.d.ts.map +1 -1
  24. package/out/src/adapters/gnaf/adapter.js +11 -10
  25. package/out/src/adapters/gnaf/adapter.js.map +1 -1
  26. package/out/src/adapters/gnaf/assemble.d.ts +4 -5
  27. package/out/src/adapters/gnaf/assemble.d.ts.map +1 -1
  28. package/out/src/adapters/gnaf/assemble.js +1 -1
  29. package/out/src/adapters/gnaf/assemble.js.map +1 -1
  30. package/out/src/adapters/index.d.ts +9 -10
  31. package/out/src/adapters/index.d.ts.map +1 -1
  32. package/out/src/adapters/index.js +13 -14
  33. package/out/src/adapters/index.js.map +1 -1
  34. package/out/src/adapters/openaddresses/adapter.d.ts +8 -10
  35. package/out/src/adapters/openaddresses/adapter.d.ts.map +1 -1
  36. package/out/src/adapters/openaddresses/adapter.js +8 -8
  37. package/out/src/adapters/openaddresses/adapter.js.map +1 -1
  38. package/out/src/adapters/overture/adapter.d.ts.map +1 -1
  39. package/out/src/adapters/overture/adapter.js +2 -2
  40. package/out/src/adapters/overture/adapter.js.map +1 -1
  41. package/out/src/adapters/state-hi-schools/adapter.d.ts +1 -1
  42. package/out/src/adapters/state-hi-schools/adapter.d.ts.map +1 -1
  43. package/out/src/adapters/state-hi-schools/adapter.js +6 -6
  44. package/out/src/adapters/state-hi-schools/adapter.js.map +1 -1
  45. package/out/src/adapters/state-ia-contractors/adapter.d.ts +1 -1
  46. package/out/src/adapters/state-ia-contractors/adapter.d.ts.map +1 -1
  47. package/out/src/adapters/state-ia-contractors/adapter.js +6 -6
  48. package/out/src/adapters/state-ia-contractors/adapter.js.map +1 -1
  49. package/out/src/adapters/state-ny-notaries/adapter.d.ts +1 -1
  50. package/out/src/adapters/state-ny-notaries/adapter.d.ts.map +1 -1
  51. package/out/src/adapters/state-ny-notaries/adapter.js +6 -6
  52. package/out/src/adapters/state-ny-notaries/adapter.js.map +1 -1
  53. package/out/src/adapters/state-tx-notaries/adapter.d.ts.map +1 -1
  54. package/out/src/adapters/state-tx-notaries/adapter.js +7 -7
  55. package/out/src/adapters/state-tx-notaries/adapter.js.map +1 -1
  56. package/out/src/adapters/synth-po-box/adapter.d.ts +9 -9
  57. package/out/src/adapters/synth-po-box/adapter.d.ts.map +1 -1
  58. package/out/src/adapters/synth-po-box/adapter.js +6 -6
  59. package/out/src/adapters/synth-po-box/adapter.js.map +1 -1
  60. package/out/src/adapters/tiger/adapter.d.ts.map +1 -1
  61. package/out/src/adapters/tiger/adapter.js +2 -2
  62. package/out/src/adapters/tiger/adapter.js.map +1 -1
  63. package/out/src/adapters/tiger/street-decompose.d.ts +2 -2
  64. package/out/src/adapters/tiger/street-decompose.d.ts.map +1 -1
  65. package/out/src/adapters/tiger/street-decompose.js +2 -2
  66. package/out/src/adapters/tiger/street-decompose.js.map +1 -1
  67. package/out/src/adapters/usgov-hrsa-fqhc/adapter.d.ts.map +1 -1
  68. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js +15 -17
  69. package/out/src/adapters/usgov-hrsa-fqhc/adapter.js.map +1 -1
  70. package/out/src/adapters/usgov-imls-pls/adapter.d.ts +1 -1
  71. package/out/src/adapters/usgov-imls-pls/adapter.d.ts.map +1 -1
  72. package/out/src/adapters/usgov-imls-pls/adapter.js +6 -6
  73. package/out/src/adapters/usgov-imls-pls/adapter.js.map +1 -1
  74. package/out/src/adapters/usgov-irs-bmf/adapter.d.ts.map +1 -1
  75. package/out/src/adapters/usgov-irs-bmf/adapter.js +5 -5
  76. package/out/src/adapters/usgov-irs-bmf/adapter.js.map +1 -1
  77. package/out/src/adapters/usgov-nad/adapter.d.ts +2 -2
  78. package/out/src/adapters/usgov-nad/adapter.d.ts.map +1 -1
  79. package/out/src/adapters/usgov-nad/adapter.js +6 -6
  80. package/out/src/adapters/usgov-nad/adapter.js.map +1 -1
  81. package/out/src/adapters/usgov-nppes/adapter.d.ts +3 -3
  82. package/out/src/adapters/usgov-nppes/adapter.d.ts.map +1 -1
  83. package/out/src/adapters/usgov-nppes/adapter.js +7 -7
  84. package/out/src/adapters/usgov-nppes/adapter.js.map +1 -1
  85. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.d.ts.map +1 -1
  86. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js +12 -12
  87. package/out/src/adapters/usgov-samhsa-treatment-locator/adapter.js.map +1 -1
  88. package/out/src/adapters/wof-admin-jp/adapter.d.ts +3 -3
  89. package/out/src/adapters/wof-admin-jp/adapter.d.ts.map +1 -1
  90. package/out/src/adapters/wof-admin-jp/adapter.js +6 -6
  91. package/out/src/adapters/wof-admin-jp/adapter.js.map +1 -1
  92. package/out/src/adapters/wof-admin-json/adapter.d.ts +17 -19
  93. package/out/src/adapters/wof-admin-json/adapter.d.ts.map +1 -1
  94. package/out/src/adapters/wof-admin-json/adapter.js +25 -28
  95. package/out/src/adapters/wof-admin-json/adapter.js.map +1 -1
  96. package/out/src/adapters/wof-postalcode-json/adapter.d.ts +8 -8
  97. package/out/src/adapters/wof-postalcode-json/adapter.d.ts.map +1 -1
  98. package/out/src/adapters/wof-postalcode-json/adapter.js +11 -11
  99. package/out/src/adapters/wof-postalcode-json/adapter.js.map +1 -1
  100. package/out/src/align.d.ts +14 -16
  101. package/out/src/align.d.ts.map +1 -1
  102. package/out/src/align.js +15 -16
  103. package/out/src/align.js.map +1 -1
  104. package/out/src/build.d.ts +13 -15
  105. package/out/src/build.d.ts.map +1 -1
  106. package/out/src/build.js +6 -6
  107. package/out/src/build.js.map +1 -1
  108. package/out/src/codex/us-fips-state.d.ts +6 -8
  109. package/out/src/codex/us-fips-state.d.ts.map +1 -1
  110. package/out/src/codex/us-fips-state.js +6 -8
  111. package/out/src/codex/us-fips-state.js.map +1 -1
  112. package/out/src/golden.d.ts.map +1 -1
  113. package/out/src/golden.js +1 -1
  114. package/out/src/golden.js.map +1 -1
  115. package/out/src/index.d.ts +1 -0
  116. package/out/src/index.d.ts.map +1 -1
  117. package/out/src/index.js +1 -0
  118. package/out/src/index.js.map +1 -1
  119. package/out/src/license.d.ts +5 -6
  120. package/out/src/license.d.ts.map +1 -1
  121. package/out/src/license.js +5 -6
  122. package/out/src/license.js.map +1 -1
  123. package/out/src/parquet-wrapper/reader.d.ts.map +1 -1
  124. package/out/src/parquet-wrapper/reader.js.map +1 -1
  125. package/out/src/parquet-wrapper/writer.d.ts.map +1 -1
  126. package/out/src/parquet-wrapper/writer.js +2 -2
  127. package/out/src/parquet-wrapper/writer.js.map +1 -1
  128. package/out/src/parquet.d.ts +16 -18
  129. package/out/src/parquet.d.ts.map +1 -1
  130. package/out/src/parquet.js +22 -23
  131. package/out/src/parquet.js.map +1 -1
  132. package/out/src/runner.d.ts +10 -10
  133. package/out/src/runner.d.ts.map +1 -1
  134. package/out/src/runner.js +8 -8
  135. package/out/src/runner.js.map +1 -1
  136. package/out/src/shard-recipes/anchor-absorption.d.ts +16 -0
  137. package/out/src/shard-recipes/anchor-absorption.d.ts.map +1 -0
  138. package/out/src/shard-recipes/anchor-absorption.js +73 -0
  139. package/out/src/shard-recipes/anchor-absorption.js.map +1 -0
  140. package/out/src/shard-recipes/boundary-stress.d.ts +16 -0
  141. package/out/src/shard-recipes/boundary-stress.d.ts.map +1 -0
  142. package/out/src/shard-recipes/boundary-stress.js +77 -0
  143. package/out/src/shard-recipes/boundary-stress.js.map +1 -0
  144. package/out/src/shard-recipes/country-balanced.d.ts +28 -0
  145. package/out/src/shard-recipes/country-balanced.d.ts.map +1 -0
  146. package/out/src/shard-recipes/country-balanced.js +370 -0
  147. package/out/src/shard-recipes/country-balanced.js.map +1 -0
  148. package/out/src/shard-recipes/fr-admin-split.d.ts +34 -0
  149. package/out/src/shard-recipes/fr-admin-split.d.ts.map +1 -0
  150. package/out/src/shard-recipes/fr-admin-split.js +175 -0
  151. package/out/src/shard-recipes/fr-admin-split.js.map +1 -0
  152. package/out/src/shard-recipes/fr-bare-street.d.ts +24 -0
  153. package/out/src/shard-recipes/fr-bare-street.d.ts.map +1 -0
  154. package/out/src/shard-recipes/fr-bare-street.js +76 -0
  155. package/out/src/shard-recipes/fr-bare-street.js.map +1 -0
  156. package/out/src/shard-recipes/fr-order.d.ts +34 -0
  157. package/out/src/shard-recipes/fr-order.d.ts.map +1 -0
  158. package/out/src/shard-recipes/fr-order.js +238 -0
  159. package/out/src/shard-recipes/fr-order.js.map +1 -0
  160. package/out/src/shard-recipes/german.d.ts +19 -0
  161. package/out/src/shard-recipes/german.d.ts.map +1 -0
  162. package/out/src/shard-recipes/german.js +170 -0
  163. package/out/src/shard-recipes/german.js.map +1 -0
  164. package/out/src/shard-recipes/house-venue.d.ts +14 -0
  165. package/out/src/shard-recipes/house-venue.d.ts.map +1 -0
  166. package/out/src/shard-recipes/house-venue.js +66 -0
  167. package/out/src/shard-recipes/house-venue.js.map +1 -0
  168. package/out/src/shard-recipes/index.d.ts +18 -0
  169. package/out/src/shard-recipes/index.d.ts.map +1 -0
  170. package/out/src/shard-recipes/index.js +58 -0
  171. package/out/src/shard-recipes/index.js.map +1 -0
  172. package/out/src/shard-recipes/intersection.d.ts +38 -0
  173. package/out/src/shard-recipes/intersection.d.ts.map +1 -0
  174. package/out/src/shard-recipes/intersection.js +473 -0
  175. package/out/src/shard-recipes/intersection.js.map +1 -0
  176. package/out/src/shard-recipes/locale.d.ts +19 -0
  177. package/out/src/shard-recipes/locale.d.ts.map +1 -0
  178. package/out/src/shard-recipes/locale.js +257 -0
  179. package/out/src/shard-recipes/locale.js.map +1 -0
  180. package/out/src/shard-recipes/no-street.d.ts +14 -0
  181. package/out/src/shard-recipes/no-street.d.ts.map +1 -0
  182. package/out/src/shard-recipes/no-street.js +66 -0
  183. package/out/src/shard-recipes/no-street.js.map +1 -0
  184. package/out/src/shard-recipes/po-box-cedex.d.ts +34 -0
  185. package/out/src/shard-recipes/po-box-cedex.d.ts.map +1 -0
  186. package/out/src/shard-recipes/po-box-cedex.js +686 -0
  187. package/out/src/shard-recipes/po-box-cedex.js.map +1 -0
  188. package/out/src/shard-recipes/po-box.d.ts +13 -0
  189. package/out/src/shard-recipes/po-box.d.ts.map +1 -0
  190. package/out/src/shard-recipes/po-box.js +93 -0
  191. package/out/src/shard-recipes/po-box.js.map +1 -0
  192. package/out/src/shard-recipes/scaffold.d.ts +104 -0
  193. package/out/src/shard-recipes/scaffold.d.ts.map +1 -0
  194. package/out/src/shard-recipes/scaffold.js +76 -0
  195. package/out/src/shard-recipes/scaffold.js.map +1 -0
  196. package/out/src/shard-recipes/street-affix.d.ts +31 -0
  197. package/out/src/shard-recipes/street-affix.d.ts.map +1 -0
  198. package/out/src/shard-recipes/street-affix.js +408 -0
  199. package/out/src/shard-recipes/street-affix.js.map +1 -0
  200. package/out/src/shard-recipes/street-bare.d.ts +21 -0
  201. package/out/src/shard-recipes/street-bare.d.ts.map +1 -0
  202. package/out/src/shard-recipes/street-bare.js +82 -0
  203. package/out/src/shard-recipes/street-bare.js.map +1 -0
  204. package/out/src/shard-recipes/street.d.ts +11 -0
  205. package/out/src/shard-recipes/street.d.ts.map +1 -0
  206. package/out/src/shard-recipes/street.js +63 -0
  207. package/out/src/shard-recipes/street.js.map +1 -0
  208. package/out/src/shard-recipes/unit.d.ts +25 -0
  209. package/out/src/shard-recipes/unit.d.ts.map +1 -0
  210. package/out/src/shard-recipes/unit.js +255 -0
  211. package/out/src/shard-recipes/unit.js.map +1 -0
  212. package/out/src/split.d.ts +23 -29
  213. package/out/src/split.d.ts.map +1 -1
  214. package/out/src/split.js +22 -29
  215. package/out/src/split.js.map +1 -1
  216. package/out/src/synthesize-anchor-absorption.d.ts +2 -3
  217. package/out/src/synthesize-anchor-absorption.d.ts.map +1 -1
  218. package/out/src/synthesize-anchor-absorption.js +2 -2
  219. package/out/src/synthesize-anchor-absorption.js.map +1 -1
  220. package/out/src/synthesize-boundary-stress.d.ts +3 -4
  221. package/out/src/synthesize-boundary-stress.d.ts.map +1 -1
  222. package/out/src/synthesize-boundary-stress.js +3 -4
  223. package/out/src/synthesize-boundary-stress.js.map +1 -1
  224. package/out/src/synthesize-german.d.ts +15 -19
  225. package/out/src/synthesize-german.d.ts.map +1 -1
  226. package/out/src/synthesize-german.js +14 -17
  227. package/out/src/synthesize-german.js.map +1 -1
  228. package/out/src/synthesize-house-venue.d.ts +2 -3
  229. package/out/src/synthesize-house-venue.d.ts.map +1 -1
  230. package/out/src/synthesize-house-venue.js +2 -3
  231. package/out/src/synthesize-house-venue.js.map +1 -1
  232. package/out/src/synthesize-intersection.d.ts +2 -2
  233. package/out/src/synthesize-intersection.d.ts.map +1 -1
  234. package/out/src/synthesize-intersection.js +4 -4
  235. package/out/src/synthesize-intersection.js.map +1 -1
  236. package/out/src/synthesize-no-street.d.ts +5 -5
  237. package/out/src/synthesize-no-street.d.ts.map +1 -1
  238. package/out/src/synthesize-no-street.js +27 -23
  239. package/out/src/synthesize-no-street.js.map +1 -1
  240. package/out/src/synthesize-po-box.d.ts +15 -17
  241. package/out/src/synthesize-po-box.d.ts.map +1 -1
  242. package/out/src/synthesize-po-box.js +27 -27
  243. package/out/src/synthesize-po-box.js.map +1 -1
  244. package/out/src/synthesize-street.d.ts +8 -9
  245. package/out/src/synthesize-street.d.ts.map +1 -1
  246. package/out/src/synthesize-street.js +3 -3
  247. package/out/src/synthesize-street.js.map +1 -1
  248. package/out/src/synthesize.d.ts +57 -66
  249. package/out/src/synthesize.d.ts.map +1 -1
  250. package/out/src/synthesize.js +56 -65
  251. package/out/src/synthesize.js.map +1 -1
  252. package/out/src/tokenize.d.ts +5 -6
  253. package/out/src/tokenize.d.ts.map +1 -1
  254. package/out/src/tokenize.js +5 -6
  255. package/out/src/tokenize.js.map +1 -1
  256. package/out/src/types.d.ts +49 -51
  257. package/out/src/types.d.ts.map +1 -1
  258. package/out/src/wof-json.d.ts +32 -34
  259. package/out/src/wof-json.d.ts.map +1 -1
  260. package/out/src/wof-json.js +23 -25
  261. package/out/src/wof-json.js.map +1 -1
  262. package/package.json +14 -14
@@ -0,0 +1,686 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `po-box-cedex` shard recipe — the po_box / cedex coverage lever of the parity campaign (the
7
+ * unit-shard playbook applied to the two starved tags `po_box` and `cedex`). Generate-mode,
8
+ * self-contained. Ported from scripts/build-po-box-cedex-shard.mjs.
9
+ *
10
+ * Surface vocabulary is provenance-first: US designators come from `@mailwoman/codex/us`
11
+ * (`isPOBox`, USPS Pub 28 §29); non-US leaders from the corpus `PO_BOX_LOCALE_TEMPLATES`;
12
+ * Canadian postcodes are synthesized to the `@mailwoman/codex/ca` pattern; CEDEX rides
13
+ * `@mailwoman/codex/fr` (`isCedex`); AU/NZ delivery services round-trip
14
+ * `@mailwoman/codex/{au,nz}`. Span convention: the WHOLE designator+number phrase is the `po_box`
15
+ * span, and "CEDEX 08" is a SEPARATE `cedex` span.
16
+ *
17
+ * Classes (CLASS_MIX): po-box-us, po-box-us-military (#517), pmb-us, bp-fr, cedex-fr, cp-ca-fr,
18
+ * po-box-ca-en, po-box-au (#517), po-box-nz (#517). `--golden` emits the leakage-safe holdout
19
+ * variant ({raw, components, country}); US golden = Vermont only, FR/CA/AU/NZ = a stable
20
+ * locality-hash holdout (hash%10===0).
21
+ *
22
+ * Prerequisites (read once, before the generation loop — these do NOT consume `random`): the cached
23
+ * OA zips in /tmp/oa-cache, the GeoNames Canada dump at /tmp/geonames-cache/CA.zip, and the
24
+ * GeoNames POSTAL-CODE dumps for AU/NZ (/tmp/geonames-cache/{AU,NZ}-postal.zip). See the legacy
25
+ * script header for the exact curl commands.
26
+ *
27
+ * Byte-fidelity: the legacy script seeded its own mulberry32 from `--seed`
28
+ * (`mulberry32(opts.seed)`); this recipe re-creates the SAME generator
29
+ * (`makeMulberry32(opts.seed)`) and preserves the synthesis call order exactly, so `--seed N`
30
+ * reproduces the legacy run byte-for-byte.
31
+ */
32
+ import { spawnSync } from "node:child_process";
33
+ import { isAuDeliveryService, isAuPostcode, isAuStateAbbreviation } from "@mailwoman/codex/au";
34
+ import { FSA_LETTER_TO_PROVINCE, normalizeCaPostalCode } from "@mailwoman/codex/ca";
35
+ import { isCedex } from "@mailwoman/codex/fr";
36
+ import { isNzDeliveryService, isNzPostcode } from "@mailwoman/codex/nz";
37
+ import { isPOBox } from "@mailwoman/codex/us";
38
+ import { alignRow } from "../align.js";
39
+ import { maybeNoisifyBoxNumber, PO_BOX_LOCALE_TEMPLATES, synthesizeMilitaryPoBoxRow, } from "../synthesize-po-box.js";
40
+ import { makeMulberry32, shardSourceID } from "./scaffold.js";
41
+ // ── Base-skeleton sources ────────────────────────────────────────────────────────────────────────
42
+ // Same OA cache as the unit/affix shards. US train = every NON-Vermont state; US eval = Vermont (the
43
+ // corpus defaultHoldout). FR comes from the BAN-derived countrywide extract (stride-sampled — the
44
+ // file is 2.5 GB and insee-ordered, so a head-only read would be all département 01).
45
+ const US_TRAIN_SOURCES = [
46
+ { zip: "/tmp/oa-cache/us__ca__berkeley.zip", csv: "us/ca/berkeley.csv", region: "CA" },
47
+ { zip: "/tmp/oa-cache/us__ca__marin.zip", csv: "us/ca/marin.csv", region: "CA" },
48
+ { zip: "/tmp/oa-cache/us__dc__statewide.zip", csv: "us/dc/statewide.csv", region: "DC" },
49
+ { zip: "/tmp/oa-cache/us__ia__statewide.zip", csv: "us/ia/statewide.csv", region: "IA" },
50
+ { zip: "/tmp/oa-cache/us__il__cook.zip", csv: "us/il/cook.csv", region: "IL" },
51
+ { zip: "/tmp/oa-cache/us__mt__statewide.zip", csv: "us/mt/statewide.csv", region: "MT" },
52
+ { zip: "/tmp/oa-cache/us__sd__statewide.zip", csv: "us/sd/statewide.csv", region: "SD" },
53
+ ];
54
+ const US_EVAL_SOURCE = { zip: "/tmp/oa-cache/us__vt__statewide.zip", csv: "us/vt/statewide.csv", region: "VT" };
55
+ const FR_SOURCE = { zip: "/tmp/oa-cache/fr__countrywide.zip", csv: "fr/countrywide.csv" };
56
+ const GEONAMES_CA = "/tmp/geonames-cache/CA.zip";
57
+ const GEONAMES_POSTAL_AU = { zip: "/tmp/geonames-cache/AU-postal.zip", txt: "AU.txt" };
58
+ const GEONAMES_POSTAL_NZ = { zip: "/tmp/geonames-cache/NZ-postal.zip", txt: "NZ.txt" };
59
+ // ── Surface vocabulary (codex + corpus templates — see the header) ──────────────────────────────
60
+ const T = Object.fromEntries(PO_BOX_LOCALE_TEMPLATES.map((t) => [t.locale, t]));
61
+ // US: the corpus en-US leaders carry the common mass; the codex-only USPS Pub-28 designators
62
+ // (Caller/Drawer/Lockbox — firm-holdout and rural forms) ride at low weight. "Box" is in both.
63
+ const US_LEADERS_COMMON = T["en-US"].leaders; // PO Box, P.O. Box, P.O.Box, PO BOX, POB, Post Office Box, Box
64
+ const US_LEADERS_RARE = ["Caller", "Firm Caller", "Drawer", "Lockbox"]; // codex US_PO_BOX_DESIGNATORS tail
65
+ // "#" EXCLUDED (v4.4.0 probe finding): bare "#N" is a secondary-unit designator per USPS Pub 28 and
66
+ // the shipped unit lever labels it `unit` — the corpus template's po_box reading CONTRADICTS a
67
+ // shipped convention. PMB stays — a genuine commercial-mail-receiving designator, no unit collision.
68
+ const US_PMB_LEADERS = T["en-US"].pmb.filter((l) => l !== "#"); // PMB
69
+ const FR_LEADERS = T["fr-FR"].leaders; // BP, B.P., Boîte Postale, BP.
70
+ const CA_FR_LEADERS = T["fr-CA"].leaders; // CP, C.P., Case Postale, BP, B.P.
71
+ const CA_EN_LEADERS = T["en-CA"].leaders; // PO Box, P.O. Box, POB, Post Office Box
72
+ // AU (#517): codex/au is the vocabulary truth. Current designators (live auspost.com.au pages) at
73
+ // full weight; the AMAS-legacy rural/community tail rides at the same 10% rare-dial as the US
74
+ // Caller/Drawer tail. Every emitted phrase must round-trip the codex matcher (makeAuNzPoBoxPhrase).
75
+ const AU_LEADERS_CURRENT = ["PO Box", "P.O. Box", "Post Office Box", "GPO Box", "Locked Bag", "Private Bag"];
76
+ const AU_LEADERS_LEGACY = ["RMB", "RSD", "CMB"]; // codex legacy: true (recognize-only forms)
77
+ // NZ (#517): the ADV358 box/bag types that carry an identifier. CMB rides rare (its "CMB B99"
78
+ // identifier shape is alpha-led, covered by makeAuNzPoBoxPhrase). Counter Delivery / Poste Restante
79
+ // are identifier-less counter services — no number to learn, excluded from synthesis.
80
+ const NZ_LEADERS_COMMON = ["PO Box", "Private Bag"];
81
+ const NZ_LEADERS_RARE = ["CMB"];
82
+ // Canadian postcode synthesis: valid first letters per province from the codex FSA prior, interior
83
+ // letters per the codex pattern (excludes the visually ambiguous D F I O Q U). The LDU digits are
84
+ // random — the SHAPE is the training signal, not the (unknowable) live assignment.
85
+ const QC_FSA_LETTERS = Object.entries(FSA_LETTER_TO_PROVINCE)
86
+ .filter(([, p]) => p === "QC")
87
+ .map(([l]) => l); // G H J
88
+ const ON_FSA_LETTERS = Object.entries(FSA_LETTER_TO_PROVINCE)
89
+ .filter(([, p]) => p === "ON")
90
+ .map(([l]) => l); // K L M N P
91
+ const CA_INTERIOR_LETTERS = "ABCEGHJKLMNPRSTVWXYZ";
92
+ // Class mix — po_box mass leans US (the production arena), cedex gets a real block, and the CA-fr
93
+ // class exists because the #511 Montréal rows ("Case Postale 200, H3A 1B9 Montréal, QC") fail today.
94
+ const CLASS_MIX = [
95
+ ["po-box-us", 0.27],
96
+ ["po-box-us-military", 0.05], // #517: CMR/PSC/Unit + Box, APO/FPO/DPO + AA/AE/AP — the arena's 0/3 class
97
+ ["pmb-us", 0.07],
98
+ ["bp-fr", 0.1],
99
+ ["cedex-fr", 0.17],
100
+ ["cp-ca-fr", 0.12],
101
+ ["po-box-ca-en", 0.04],
102
+ ["po-box-au", 0.12],
103
+ ["po-box-nz", 0.06],
104
+ ];
105
+ /** Synthetic recipient/venue prefixes — the arena's "JOHN DOE, ACME INC, …" pattern. */
106
+ const VENUES_EN = ["John Doe", "Jane Smith", "Acme Inc", "Wayne Enterprises", "Maria Garcia", "Riverside Clinic"];
107
+ const VENUES_FR = ["Société Dupont", "Cabinet Martin", "Hôpital Central", "Mairie Annexe", "Imprimerie Moderne"];
108
+ // ── Holdout + CSV helpers ────────────────────────────────────────────────────────────────────────
109
+ /** Stable locality hash for the FR/CA train↔golden split (djb2; hash%10===0 → golden-only). */
110
+ function localityHash(name) {
111
+ let h = 5381;
112
+ const s = name.toLowerCase();
113
+ for (let i = 0; i < s.length; i++)
114
+ h = ((h << 5) + h + s.charCodeAt(i)) >>> 0;
115
+ return h;
116
+ }
117
+ const isHoldoutLocality = (name) => localityHash(name) % 10 === 0;
118
+ /** Minimal RFC-4180-ish splitter (handles quoted fields). */
119
+ function splitCSV(line) {
120
+ const out = [];
121
+ let cur = "";
122
+ let inQ = false;
123
+ for (let i = 0; i < line.length; i++) {
124
+ const c = line[i];
125
+ if (inQ) {
126
+ if (c === '"') {
127
+ if (line[i + 1] === '"') {
128
+ cur += '"';
129
+ i++;
130
+ }
131
+ else
132
+ inQ = false;
133
+ }
134
+ else
135
+ cur += c;
136
+ }
137
+ else if (c === '"')
138
+ inQ = true;
139
+ else if (c === ",") {
140
+ out.push(cur);
141
+ cur = "";
142
+ }
143
+ else
144
+ cur += c;
145
+ }
146
+ out.push(cur);
147
+ return out;
148
+ }
149
+ const cleanLocality = (loc) => loc && loc.length <= 40 && !/\d|,/.test(loc) && !/cedex/i.test(loc);
150
+ /** Stream real US tuples (number/street/city/postcode) out of a cached OA zip. */
151
+ function readUsTuples(source) {
152
+ const r = spawnSync("unzip", ["-p", source.zip, source.csv], { maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" });
153
+ if (r.status !== 0) {
154
+ console.error(` WARN: unzip failed for ${source.zip} (status ${r.status})`);
155
+ return [];
156
+ }
157
+ const lines = r.stdout.toString("utf8").split(/\r?\n/);
158
+ if (lines.length < 2)
159
+ return [];
160
+ const header = splitCSV(lines[0]).map((h) => h.trim().toLowerCase());
161
+ const idx = (name) => header.indexOf(name);
162
+ const iNum = idx("number"), iStreet = idx("street"), iCity = idx("city"), iPost = idx("postcode");
163
+ const get = (cells, i) => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "");
164
+ const tuples = [];
165
+ const seen = new Set();
166
+ for (let li = 1; li < lines.length; li++) {
167
+ if (!lines[li])
168
+ continue;
169
+ const cells = splitCSV(lines[li]);
170
+ const locality = get(cells, iCity);
171
+ if (!cleanLocality(locality))
172
+ continue;
173
+ const key = locality.toLowerCase();
174
+ const street = get(cells, iStreet), house_number = get(cells, iNum);
175
+ // One tuple per (locality, street) pair keeps the pool varied without ballooning memory.
176
+ const pairKey = `${key}|${street}`.toLowerCase();
177
+ if (seen.has(pairKey))
178
+ continue;
179
+ seen.add(pairKey);
180
+ tuples.push({ house_number, street, locality, region: source.region, postcode: get(cells, iPost) });
181
+ }
182
+ return tuples;
183
+ }
184
+ /**
185
+ * Stride-sampled FR tuples (number/street/city/postcode). The countrywide CSV is 2.5 GB and insee-ordered; `awk NR%K`
186
+ * strides the whole country instead of reading one département. Quoted commas survive because awk only FILTERS lines —
187
+ * parsing stays in splitCSV.
188
+ */
189
+ function readFrTuples(limit) {
190
+ const r = spawnSync("bash", ["-c", `unzip -p "${FR_SOURCE.zip}" "${FR_SOURCE.csv}" | awk 'NR==1 || NR%211==3' | head -n ${limit + 1}`], { maxBuffer: 1024 * 1024 * 1024, encoding: "buffer" });
191
+ if (r.status !== 0 && r.stdout.length === 0) {
192
+ console.error(` WARN: unzip failed for ${FR_SOURCE.zip}`);
193
+ return [];
194
+ }
195
+ const lines = r.stdout.toString("utf8").split(/\r?\n/);
196
+ if (lines.length < 2)
197
+ return [];
198
+ const header = splitCSV(lines[0]).map((h) => h.trim().toLowerCase());
199
+ const idx = (n) => header.indexOf(n);
200
+ const iNum = idx("number"), iStreet = idx("street"), iCity = idx("city"), iPost = idx("postcode");
201
+ const get = (cells, i) => (i >= 0 && i < cells.length ? (cells[i] ?? "").trim() : "");
202
+ const tuples = [];
203
+ const seen = new Set();
204
+ for (let li = 1; li < lines.length; li++) {
205
+ if (!lines[li])
206
+ continue;
207
+ const cells = splitCSV(lines[li]);
208
+ const locality = get(cells, iCity), postcode = get(cells, iPost), street = get(cells, iStreet), house_number = get(cells, iNum);
209
+ if (!cleanLocality(locality) || !/^\d{5}$/.test(postcode) || !street || !house_number)
210
+ continue;
211
+ const key = `${locality}|${street}`.toLowerCase();
212
+ if (seen.has(key))
213
+ continue;
214
+ seen.add(key);
215
+ tuples.push({ house_number, street, locality, postcode });
216
+ }
217
+ return tuples;
218
+ }
219
+ /**
220
+ * Canadian locality pools from the GeoNames dump (CC-BY 4.0): feature class P, admin1 10 (Québec) / 08 (Ontario),
221
+ * population > 1000. GeoNames is the provenance-tracked source — no hand list.
222
+ */
223
+ function readCaLocalities(admin1) {
224
+ const r = spawnSync("bash", [
225
+ "-c",
226
+ `unzip -p "${GEONAMES_CA}" CA.txt | awk -F'\\t' '$7=="P" && $9=="CA" && $11=="${admin1}" && $15>1000 {print $2}'`,
227
+ ], { maxBuffer: 1024 * 1024 * 256, encoding: "utf8" });
228
+ if (r.status !== 0) {
229
+ console.error(` WARN: GeoNames read failed (admin1=${admin1}) — is ${GEONAMES_CA} present?`);
230
+ return [];
231
+ }
232
+ return [...new Set(r.stdout.split("\n").filter(cleanLocality))];
233
+ }
234
+ /**
235
+ * Real (locality, state?, postcode) tuples from a GeoNames postal-code dump (CC-BY 4.0). Tab format: country,
236
+ * postal_code, place_name, admin1_name, admin1_code, … — for AU the admin1_code IS the postal state abbreviation
237
+ * (NSW/VIC/…), validated against the codex table; NZ has no region line so the state column is ignored. Postcodes are
238
+ * validated against the codex 4-digit shape — a dump row that fails the contract is skipped, not emitted as a junk
239
+ * label.
240
+ */
241
+ function readPostalTuples(source, opts) {
242
+ const r = spawnSync("unzip", ["-p", source.zip, source.txt], { maxBuffer: 1024 * 1024 * 64, encoding: "utf8" });
243
+ if (r.status !== 0) {
244
+ console.error(` WARN: unzip failed for ${source.zip} (status ${r.status})`);
245
+ return [];
246
+ }
247
+ const tuples = [];
248
+ const seen = new Set();
249
+ const validPostcode = opts.withState ? isAuPostcode : isNzPostcode;
250
+ for (const line of r.stdout.split("\n")) {
251
+ if (!line)
252
+ continue;
253
+ const cols = line.split("\t");
254
+ const postcode = (cols[1] ?? "").trim();
255
+ const locality = (cols[2] ?? "").trim();
256
+ const region = (cols[4] ?? "").trim();
257
+ if (!cleanLocality(locality) || !validPostcode(postcode))
258
+ continue;
259
+ if (opts.withState && !isAuStateAbbreviation(region))
260
+ continue;
261
+ const key = `${locality}|${postcode}`.toLowerCase();
262
+ if (seen.has(key))
263
+ continue;
264
+ seen.add(key);
265
+ tuples.push(opts.withState ? { locality, region, postcode } : { locality, postcode });
266
+ }
267
+ return tuples;
268
+ }
269
+ // ── Rendering helpers ────────────────────────────────────────────────────────────────────────────
270
+ /** Box-number distribution (mirrors the corpus defaultPickNumber bands: 70% are 1-3 digits). */
271
+ function pickBoxNumber(random) {
272
+ const r = random();
273
+ if (r < 0.3)
274
+ return String(1 + Math.floor(random() * 99));
275
+ if (r < 0.7)
276
+ return String(100 + Math.floor(random() * 900));
277
+ if (r < 0.95)
278
+ return String(1000 + Math.floor(random() * 9000));
279
+ return String(10000 + Math.floor(random() * 90000));
280
+ }
281
+ /** Case dial for the designator phrase: mostly template casing, sometimes UPPER, rarely lower. */
282
+ function caseDial(random, s) {
283
+ const r = random();
284
+ if (r < 0.7)
285
+ return s;
286
+ if (r < 0.92)
287
+ return s.toUpperCase();
288
+ return s.toLowerCase();
289
+ }
290
+ // Leaders the codex PO_BOX_RE genuinely covers (everything en-US except "POB"; PMB/"#" are the
291
+ // corpus's CMRA forms, outside USPS Pub-28 §29). Used to scope the isPOBox round-trip assertion.
292
+ const CODEX_COVERED_LEADERS = new Set(["PO Box", "P.O. Box", "P.O.Box", "PO BOX", "Post Office Box", "Box", ...US_LEADERS_RARE].map((l) => l.toLowerCase()));
293
+ /** Compose a po_box phrase. "#" joins without a space ("#500", the golden PMB variant). */
294
+ function makePoBoxPhrase(random, leaders, rareLeaders) {
295
+ let leader = leaders[Math.floor(random() * leaders.length)];
296
+ if (rareLeaders && random() < 0.1)
297
+ leader = rareLeaders[Math.floor(random() * rareLeaders.length)];
298
+ const num = maybeNoisifyBoxNumber(pickBoxNumber(random), random);
299
+ const phrase = leader === "#" ? `#${num}` : `${caseDial(random, leader)} ${num}`;
300
+ // Codex round-trip: a phrase built from a codex-known designator and a clean id must satisfy the
301
+ // matcher (the noisy ids — commas/spaces — are corpus-designed adversarial forms the regex rightly
302
+ // rejects, so they're exempt). A failure here is a generation bug; fail loud.
303
+ if (CODEX_COVERED_LEADERS.has(leader.toLowerCase()) && /^[\dA-Za-z][\dA-Za-z-]*$/.test(num) && !isPOBox(phrase)) {
304
+ throw new Error(`generated a po_box phrase the codex matcher rejects: "${phrase}"`);
305
+ }
306
+ return phrase;
307
+ }
308
+ /**
309
+ * A CEDEX designation: "CEDEX 08" / "Cedex 8" / bare "CEDEX". Shape contract = codex fr/cedex — every emitted phrase
310
+ * must satisfy isCedex, loud.
311
+ */
312
+ function makeCedex(random) {
313
+ const r = random();
314
+ const word = r < 0.6 ? "CEDEX" : r < 0.9 ? "Cedex" : "cedex";
315
+ const phrase = (() => {
316
+ if (random() < 0.2)
317
+ return word; // "33077 BORDEAUX CEDEX" — un-numbered offices are common
318
+ const n = 1 + Math.floor(random() * 20);
319
+ const id = random() < 0.5 ? String(n).padStart(2, "0") : String(n);
320
+ return `${word} ${id}`;
321
+ })();
322
+ if (!isCedex(phrase))
323
+ throw new Error(`makeCedex emitted a phrase the codex matcher rejects: "${phrase}"`);
324
+ return phrase;
325
+ }
326
+ /**
327
+ * Compose an AU or NZ delivery-service phrase from the codex-sourced leaders. Same contract as makePoBoxPhrase: a
328
+ * phrase built from a codex-known designator and a clean id must round-trip the codex matcher (isAuDeliveryService /
329
+ * isNzDeliveryService) — a failure is a generation bug, loud. Noisy ids (commas / embedded spaces) are corpus-designed
330
+ * adversarial forms, exempt.
331
+ */
332
+ function makeAuNzPoBoxPhrase(random, leaders, rareLeaders, validate) {
333
+ let leader = leaders[Math.floor(random() * leaders.length)];
334
+ if (rareLeaders && random() < 0.1)
335
+ leader = rareLeaders[Math.floor(random() * rareLeaders.length)];
336
+ let num = maybeNoisifyBoxNumber(pickBoxNumber(random), random);
337
+ // NZ CMB identifiers are alpha-led per the ADV358 example ("CMB B99").
338
+ if (leader === "CMB" && validate === isNzDeliveryService)
339
+ num = `B${num}`;
340
+ const phrase = `${caseDial(random, leader)} ${num}`;
341
+ // The "clean id" shape differs per system: ADV358 identifiers carry no separators at all, the AU
342
+ // AMAS id (like the US one) tolerates dashes. Noisy ids outside the clean shape are exempt.
343
+ const cleanID = validate === isNzDeliveryService ? /^[\dA-Za-z]+$/ : /^[\dA-Za-z][\dA-Za-z-]*$/;
344
+ if (cleanID.test(num) && !validate(phrase)) {
345
+ throw new Error(`generated a phrase the codex matcher rejects: "${phrase}"`);
346
+ }
347
+ return phrase;
348
+ }
349
+ /** Synthesize a codex-valid Canadian postcode for a province's FSA letters ("H2X 3V4"). */
350
+ function makeCaPostcode(random, fsaLetters) {
351
+ const L = () => CA_INTERIOR_LETTERS[Math.floor(random() * CA_INTERIOR_LETTERS.length)];
352
+ const D = () => String(Math.floor(random() * 10));
353
+ const first = fsaLetters[Math.floor(random() * fsaLetters.length)];
354
+ const pc = `${first}${D()}${L()} ${D()}${L()}${D()}`;
355
+ // The codex pattern is the contract — a generation bug should fail loud, not emit junk labels.
356
+ if (!normalizeCaPostalCode(pc))
357
+ throw new Error(`generated an invalid CA postcode: ${pc}`);
358
+ return pc;
359
+ }
360
+ const pick = (random, arr) => arr[Math.floor(random() * arr.length)];
361
+ // ── Per-class renderers — each returns { fmt, raw, components } ──────────────────────────────────
362
+ function renderPoBoxUs(random, t) {
363
+ const phrase = makePoBoxPhrase(random, US_LEADERS_COMMON, US_LEADERS_RARE);
364
+ const { locality: loc, region: reg, postcode: pc } = t;
365
+ const base = { po_box: phrase, locality: loc, region: reg };
366
+ const r = random();
367
+ if (r < 0.4 && pc)
368
+ return { fmt: "full", raw: `${phrase}, ${loc}, ${reg} ${pc}`, components: { ...base, postcode: pc } };
369
+ if (r < 0.55)
370
+ return { fmt: "no-postcode", raw: `${phrase}, ${loc}, ${reg}`, components: base };
371
+ if (r < 0.75)
372
+ return { fmt: "bare", raw: phrase, components: { po_box: phrase } };
373
+ if (r < 0.9) {
374
+ const v = pick(random, VENUES_EN);
375
+ return {
376
+ fmt: "venue",
377
+ raw: pc ? `${v}, ${phrase}, ${loc}, ${reg} ${pc}` : `${v}, ${phrase}, ${loc}, ${reg}`,
378
+ components: { venue: v, ...base, ...(pc ? { postcode: pc } : {}) },
379
+ };
380
+ }
381
+ // USPS label form: comma-less, all-caps ("PO BOX 123 BURLINGTON VT 05401").
382
+ const up = (s) => s.toUpperCase();
383
+ return {
384
+ fmt: "label-nocomma",
385
+ raw: pc ? `${up(phrase)} ${up(loc)} ${reg} ${pc}` : `${up(phrase)} ${up(loc)} ${reg}`,
386
+ components: { po_box: up(phrase), locality: up(loc), region: reg, ...(pc ? { postcode: pc } : {}) },
387
+ };
388
+ }
389
+ function renderPmbUs(random, t) {
390
+ const phrase = makePoBoxPhrase(random, US_PMB_LEADERS);
391
+ const { house_number: hn, street, locality: loc, region: reg, postcode: pc } = t;
392
+ const road = `${hn} ${street}`;
393
+ const components = { house_number: hn, street, po_box: phrase, locality: loc, region: reg, postcode: pc };
394
+ const r = random();
395
+ if (r < 0.5)
396
+ return { fmt: "pmb-after-street", raw: `${road} ${phrase}, ${loc}, ${reg} ${pc}`, components };
397
+ if (r < 0.85)
398
+ return { fmt: "pmb-comma", raw: `${road}, ${phrase}, ${loc}, ${reg} ${pc}`, components };
399
+ return { fmt: "pmb-bare", raw: `${road} ${phrase}`, components: { house_number: hn, street, po_box: phrase } };
400
+ }
401
+ function renderBpFr(random, t) {
402
+ const phrase = makePoBoxPhrase(random, FR_LEADERS);
403
+ const { locality, postcode: pc } = t;
404
+ const upper = random() < 0.5;
405
+ const loc = upper ? locality.toUpperCase() : locality;
406
+ const r = random();
407
+ if (r < 0.45)
408
+ return {
409
+ fmt: "bp-tail",
410
+ raw: `${phrase}, ${pc} ${loc}`,
411
+ components: { po_box: phrase, postcode: pc, locality: loc },
412
+ };
413
+ if (r < 0.6)
414
+ return { fmt: "bp-bare", raw: phrase, components: { po_box: phrase } };
415
+ if (r < 0.8) {
416
+ // The institutional combo line — a BP and a CEDEX routing on the same last line.
417
+ const cedex = makeCedex(random);
418
+ const locUp = locality.toUpperCase();
419
+ return {
420
+ fmt: "bp-cedex",
421
+ raw: `${phrase}, ${pc} ${locUp} ${cedex}`,
422
+ components: { po_box: phrase, postcode: pc, locality: locUp, cedex },
423
+ };
424
+ }
425
+ const v = pick(random, VENUES_FR);
426
+ return {
427
+ fmt: "bp-venue",
428
+ raw: `${v}, ${phrase}, ${pc} ${loc}`,
429
+ components: { venue: v, po_box: phrase, postcode: pc, locality: loc },
430
+ };
431
+ }
432
+ function renderCedexFr(random, t) {
433
+ const cedex = makeCedex(random);
434
+ const { house_number: hn, street, locality, postcode: pc } = t;
435
+ const loc = random() < 0.6 ? locality.toUpperCase() : locality;
436
+ const line = { postcode: pc, locality: loc, cedex };
437
+ const r = random();
438
+ if (r < 0.4)
439
+ return { fmt: "cedex-line", raw: `${pc} ${loc} ${cedex}`, components: line };
440
+ if (r < 0.75)
441
+ return {
442
+ fmt: "cedex-full",
443
+ raw: `${hn} ${street}, ${pc} ${loc} ${cedex}`,
444
+ components: { house_number: hn, street, ...line },
445
+ };
446
+ if (r < 0.85)
447
+ return { fmt: "cedex-golden-order", raw: `${pc} ${cedex} ${loc}`, components: line };
448
+ const v = pick(random, VENUES_FR);
449
+ return { fmt: "cedex-venue", raw: `${v}, ${pc} ${loc} ${cedex}`, components: { venue: v, ...line } };
450
+ }
451
+ function renderCaFr(random, loc) {
452
+ const phrase = makePoBoxPhrase(random, CA_FR_LEADERS);
453
+ const pc = makeCaPostcode(random, QC_FSA_LETTERS);
454
+ const components = { po_box: phrase, postcode: pc, locality: loc, region: "QC" };
455
+ const r = random();
456
+ // The #511 golden order: postcode BEFORE locality, region trailing.
457
+ if (r < 0.4)
458
+ return { fmt: "ca-fr-golden-order", raw: `${phrase}, ${pc} ${loc}, QC`, components };
459
+ if (r < 0.7)
460
+ return { fmt: "ca-fr-native", raw: `${phrase}, ${loc} QC ${pc}`, components };
461
+ if (r < 0.85)
462
+ return { fmt: "ca-fr-bare", raw: phrase, components: { po_box: phrase } };
463
+ const v = pick(random, VENUES_FR);
464
+ return { fmt: "ca-fr-venue", raw: `${v}, ${phrase}, ${loc} QC ${pc}`, components: { venue: v, ...components } };
465
+ }
466
+ function renderCaEn(random, loc) {
467
+ const phrase = makePoBoxPhrase(random, CA_EN_LEADERS);
468
+ const pc = makeCaPostcode(random, ON_FSA_LETTERS);
469
+ const components = { po_box: phrase, locality: loc, region: "ON", postcode: pc };
470
+ const r = random();
471
+ if (r < 0.5)
472
+ return { fmt: "ca-en-standard", raw: `${phrase}, ${loc}, ON ${pc}`, components };
473
+ if (r < 0.8)
474
+ return { fmt: "ca-en-golden-order", raw: `${phrase}, ${pc} ${loc}, ON`, components };
475
+ return { fmt: "ca-en-bare", raw: phrase, components: { po_box: phrase } };
476
+ }
477
+ function renderAuPoBox(random, t) {
478
+ const phrase = makeAuNzPoBoxPhrase(random, AU_LEADERS_CURRENT, AU_LEADERS_LEGACY, isAuDeliveryService);
479
+ const { locality, region: reg, postcode: pc } = t;
480
+ const r = random();
481
+ // The guideline last line is capitals ("SYDNEY NSW 2000"); mixed case rides as a softer variant.
482
+ const loc = r < 0.6 ? locality.toUpperCase() : locality;
483
+ const base = { po_box: phrase, locality: loc, region: reg, postcode: pc };
484
+ if (r < 0.45)
485
+ return { fmt: "au-standard", raw: `${phrase}, ${loc} ${reg} ${pc}`, components: base };
486
+ if (r < 0.6) {
487
+ // The envelope label form: comma-less, designator upper-cased ("GPO BOX 123 SYDNEY NSW 2001").
488
+ const up = phrase.toUpperCase();
489
+ return {
490
+ fmt: "au-label-nocomma",
491
+ raw: `${up} ${locality.toUpperCase()} ${reg} ${pc}`,
492
+ components: { po_box: up, locality: locality.toUpperCase(), region: reg, postcode: pc },
493
+ };
494
+ }
495
+ if (r < 0.75)
496
+ return {
497
+ fmt: "au-no-postcode",
498
+ raw: `${phrase}, ${loc} ${reg}`,
499
+ components: { po_box: phrase, locality: loc, region: reg },
500
+ };
501
+ if (r < 0.88)
502
+ return { fmt: "au-bare", raw: phrase, components: { po_box: phrase } };
503
+ const v = pick(random, VENUES_EN);
504
+ return { fmt: "au-venue", raw: `${v}, ${phrase}, ${loc} ${reg} ${pc}`, components: { venue: v, ...base } };
505
+ }
506
+ function renderNzPoBox(random, t) {
507
+ const phrase = makeAuNzPoBoxPhrase(random, NZ_LEADERS_COMMON, NZ_LEADERS_RARE, isNzDeliveryService);
508
+ const { locality, postcode: pc } = t;
509
+ // NZ addresses are written mixed-case ("PO Box 4099, Timaru 7942") — no region line (ADV358).
510
+ const base = { po_box: phrase, locality, postcode: pc };
511
+ const r = random();
512
+ if (r < 0.55)
513
+ return { fmt: "nz-standard", raw: `${phrase}, ${locality} ${pc}`, components: base };
514
+ if (r < 0.7)
515
+ return { fmt: "nz-no-postcode", raw: `${phrase}, ${locality}`, components: { po_box: phrase, locality } };
516
+ if (r < 0.85)
517
+ return { fmt: "nz-bare", raw: phrase, components: { po_box: phrase } };
518
+ const v = pick(random, VENUES_EN);
519
+ return { fmt: "nz-venue", raw: `${v}, ${phrase}, ${locality} ${pc}`, components: { venue: v, ...base } };
520
+ }
521
+ // ── Component ordering ───────────────────────────────────────────────────────────────────────────
522
+ /**
523
+ * Order components so short, collision-prone needles (2-letter regions) are located AFTER the long anchored ones —
524
+ * alignRow claims spans greedily in insertion order, and a leading "on" inside "London" must already be claimed by
525
+ * `locality` before `region: "ON"` goes looking.
526
+ */
527
+ const COMPONENT_ORDER = ["house_number", "street", "po_box", "venue", "locality", "postcode", "region", "cedex"];
528
+ function orderComponents(components) {
529
+ const out = {};
530
+ for (const k of COMPONENT_ORDER) {
531
+ const v = components[k];
532
+ if (v)
533
+ out[k] = v;
534
+ }
535
+ return out;
536
+ }
537
+ export const poBoxCedexRecipe = {
538
+ name: "po-box-cedex",
539
+ description: "PO box / CEDEX coverage rows (US/FR/CA/AU/NZ) — self-generated from cached OA + GeoNames pools",
540
+ mode: "generate",
541
+ options: [{ flag: "--golden", description: "Emit the leakage-safe holdout variant ({raw, components, country})" }],
542
+ async run(opts, write) {
543
+ const random = makeMulberry32(opts.seed);
544
+ const count = opts.count ?? 50000;
545
+ const source = opts.sourceName ?? "synth-po-box-cedex";
546
+ // US pool: VT only for golden, non-VT for train (the established geographic holdout).
547
+ const usPool = [];
548
+ for (const s of opts.golden ? [US_EVAL_SOURCE] : US_TRAIN_SOURCES) {
549
+ const t = readUsTuples(s);
550
+ console.error(` ${s.csv}: ${t.length} tuples`);
551
+ for (const x of t)
552
+ usPool.push(x);
553
+ }
554
+ // FR + CA pools: stable locality-hash holdout (golden gets hash%10==0, train the rest).
555
+ const frAll = readFrTuples(80000);
556
+ const frPool = frAll.filter((t) => isHoldoutLocality(t.locality) === opts.golden);
557
+ console.error(` ${FR_SOURCE.csv}: ${frAll.length} tuples (${frPool.length} after holdout split)`);
558
+ const qcAll = readCaLocalities("10");
559
+ const onAll = readCaLocalities("08");
560
+ const qcPool = qcAll.filter((l) => isHoldoutLocality(l) === opts.golden);
561
+ const onPool = onAll.filter((l) => isHoldoutLocality(l) === opts.golden);
562
+ console.error(` GeoNames CA: QC ${qcAll.length}→${qcPool.length}, ON ${onAll.length}→${onPool.length}`);
563
+ // AU/NZ pools: same stable locality-hash holdout as FR/CA.
564
+ const auAll = readPostalTuples(GEONAMES_POSTAL_AU, { withState: true });
565
+ const nzAll = readPostalTuples(GEONAMES_POSTAL_NZ, { withState: false });
566
+ const auPool = auAll.filter((t) => isHoldoutLocality(t.locality) === opts.golden);
567
+ const nzPool = nzAll.filter((t) => isHoldoutLocality(t.locality) === opts.golden);
568
+ console.error(` GeoNames postal: AU ${auAll.length}→${auPool.length}, NZ ${nzAll.length}→${nzPool.length}`);
569
+ if (usPool.length === 0 ||
570
+ frPool.length === 0 ||
571
+ qcPool.length === 0 ||
572
+ onPool.length === 0 ||
573
+ auPool.length === 0 ||
574
+ nzPool.length === 0) {
575
+ throw new Error("A base pool is empty — check /tmp/oa-cache and /tmp/geonames-cache (CA.zip, AU-postal.zip, NZ-postal.zip).");
576
+ }
577
+ const pickClass = (r) => {
578
+ let acc = 0;
579
+ for (const [name, w] of CLASS_MIX) {
580
+ acc += w;
581
+ if (r < acc)
582
+ return name;
583
+ }
584
+ return CLASS_MIX[CLASS_MIX.length - 1][0];
585
+ };
586
+ let emitted = 0;
587
+ let skipped = 0;
588
+ let guard = 0;
589
+ while (emitted < count && guard++ < count * 10) {
590
+ const cls = pickClass(random());
591
+ let rendered;
592
+ let country;
593
+ let locale;
594
+ if (cls === "po-box-us") {
595
+ rendered = renderPoBoxUs(random, pick(random, usPool));
596
+ country = "US";
597
+ locale = "en-US";
598
+ }
599
+ else if (cls === "pmb-us") {
600
+ const t = pick(random, usPool);
601
+ if (!t.postcode || !t.street || !t.house_number)
602
+ continue;
603
+ rendered = renderPmbUs(random, t);
604
+ country = "US";
605
+ locale = "en-US";
606
+ }
607
+ else if (cls === "bp-fr") {
608
+ rendered = renderBpFr(random, pick(random, frPool));
609
+ country = "FR";
610
+ locale = "fr-FR";
611
+ }
612
+ else if (cls === "cedex-fr") {
613
+ rendered = renderCedexFr(random, pick(random, frPool));
614
+ country = "FR";
615
+ locale = "fr-FR";
616
+ }
617
+ else if (cls === "cp-ca-fr") {
618
+ rendered = renderCaFr(random, pick(random, qcPool));
619
+ country = "CA";
620
+ locale = "fr-CA";
621
+ }
622
+ else if (cls === "po-box-au") {
623
+ rendered = renderAuPoBox(random, pick(random, auPool));
624
+ country = "AU";
625
+ locale = "en-AU";
626
+ }
627
+ else if (cls === "po-box-nz") {
628
+ rendered = renderNzPoBox(random, pick(random, nzPool));
629
+ country = "NZ";
630
+ locale = "en-NZ";
631
+ }
632
+ else if (cls === "po-box-us-military") {
633
+ // #517: self-contained (no real-tuple tail) — APO/FPO/DPO locality + AA/AE/AP region + theatre
634
+ // ZIP, codex-backed. Strip the synthesizer's `country` field (the build sets country below).
635
+ const m = synthesizeMilitaryPoBoxRow({ random });
636
+ const { country: _c, ...comps } = m.components;
637
+ rendered = { fmt: "po-box-military", raw: m.raw, components: comps };
638
+ country = "US";
639
+ locale = "en-US";
640
+ }
641
+ else {
642
+ rendered = renderCaEn(random, pick(random, onPool));
643
+ country = "CA";
644
+ locale = "en-CA";
645
+ }
646
+ const { raw, components } = rendered;
647
+ // Every component surface must survive verbatim in raw, else alignment can't label it.
648
+ if (!Object.values(components).every((s) => raw.includes(s))) {
649
+ skipped++;
650
+ continue;
651
+ }
652
+ if (opts.golden) {
653
+ write(JSON.stringify({ raw, components: orderComponents(components), country }) + "\n");
654
+ emitted++;
655
+ continue;
656
+ }
657
+ const canonical = {
658
+ raw,
659
+ components: orderComponents(components),
660
+ country,
661
+ locale,
662
+ source,
663
+ source_id: shardSourceID(source, components),
664
+ corpus_version: "0.4.0",
665
+ license: country === "CA"
666
+ ? "GeoNames CA (CC-BY 4.0) locality skeletons + Canada Post box forms (corpus templates); postcodes synthesized to the codex CA pattern"
667
+ : country === "FR"
668
+ ? "OpenAddresses FR (BAN-derived) skeletons + La Poste BP/CEDEX forms (corpus templates, NF Z 10-011)"
669
+ : country === "AU"
670
+ ? "GeoNames AU postal dump (CC-BY 4.0) locality/state/postcode tails + Australia Post Postal Delivery Type designators (@mailwoman/codex/au)"
671
+ : country === "NZ"
672
+ ? "GeoNames NZ postal dump (CC-BY 4.0) locality/postcode tails + NZ Post ADV358 Delivery Service Types (@mailwoman/codex/nz)"
673
+ : "OpenAddresses US (non-VT) skeletons + USPS Pub-28 §29 PO-box designators (codex/corpus templates)",
674
+ };
675
+ const aligned = alignRow(canonical);
676
+ if (aligned.kind !== "labeled" || !aligned.row) {
677
+ skipped++;
678
+ continue;
679
+ }
680
+ write(JSON.stringify({ ...aligned.row, synth_method: cls, synth_base_id: null }) + "\n");
681
+ emitted++;
682
+ }
683
+ return { emitted, skipped };
684
+ },
685
+ };
686
+ //# sourceMappingURL=po-box-cedex.js.map