@mailwoman/codex 9.3.0 → 10.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 (223) hide show
  1. package/README.md +221 -75
  2. package/lib/address/format.ts +144 -0
  3. package/lib/address/key.ts +134 -0
  4. package/lib/address/layout.ts +189 -0
  5. package/lib/address/layouts/generated.ts +1576 -0
  6. package/lib/address/layouts/index.ts +213 -0
  7. package/lib/address/render.ts +176 -0
  8. package/lib/{address-system-conventions.ts → address/system-conventions.ts} +6 -5
  9. package/lib/component.ts +96 -0
  10. package/lib/country/index.ts +3 -2
  11. package/lib/country/{reference.ts → reference/index.ts} +1 -1
  12. package/lib/country/region-languages.ts +46 -0
  13. package/lib/es/co-official-languages.ts +62 -0
  14. package/lib/es/index.ts +1 -0
  15. package/lib/gb/index.ts +2 -2
  16. package/lib/index.ts +4 -2
  17. package/lib/jp/index.ts +1 -0
  18. package/lib/jp/municipality-register.ts +62 -0
  19. package/lib/kr/index.ts +10 -0
  20. package/lib/kr/sigungu-register.ts +294 -0
  21. package/lib/placetype-map.ts +157 -0
  22. package/lib/postcode/shapes.json +80 -0
  23. package/lib/postcode/shapes.ts +56 -0
  24. package/lib/{postcode-systems.ts → postcode/systems.ts} +8 -1
  25. package/lib/register-completion.ts +24 -0
  26. package/lib/us/index.ts +2 -2
  27. package/lib/us/po-box.ts +37 -10
  28. package/lib/us/{street-directional.ts → street/directional.ts} +1 -1
  29. package/lib/us/{street-suffix.ts → street/suffix.ts} +1 -1
  30. package/out/address/format.d.ts +87 -0
  31. package/out/address/format.d.ts.map +1 -0
  32. package/out/address/format.js +85 -0
  33. package/out/address/format.js.map +1 -0
  34. package/out/address/key.d.ts +67 -0
  35. package/out/address/key.d.ts.map +1 -0
  36. package/out/address/key.js +91 -0
  37. package/out/address/key.js.map +1 -0
  38. package/out/address/layout.d.ts +104 -0
  39. package/out/address/layout.d.ts.map +1 -0
  40. package/out/address/layout.js +123 -0
  41. package/out/address/layout.js.map +1 -0
  42. package/out/address/layouts/generated.d.ts +21 -0
  43. package/out/address/layouts/generated.d.ts.map +1 -0
  44. package/out/address/layouts/generated.js +1380 -0
  45. package/out/address/layouts/generated.js.map +1 -0
  46. package/out/address/layouts/index.d.ts +81 -0
  47. package/out/address/layouts/index.d.ts.map +1 -0
  48. package/out/address/layouts/index.js +184 -0
  49. package/out/address/layouts/index.js.map +1 -0
  50. package/out/address/render.d.ts +64 -0
  51. package/out/address/render.d.ts.map +1 -0
  52. package/out/address/render.js +115 -0
  53. package/out/address/render.js.map +1 -0
  54. package/out/{address-system-conventions.d.ts → address/system-conventions.d.ts} +6 -5
  55. package/out/address/system-conventions.d.ts.map +1 -0
  56. package/out/{address-system-conventions.js → address/system-conventions.js} +2 -2
  57. package/out/address/system-conventions.js.map +1 -0
  58. package/out/au/delivery-service.d.ts.map +1 -1
  59. package/out/au/level-designator.d.ts.map +1 -1
  60. package/out/au/state.d.ts.map +1 -1
  61. package/out/ca/province.d.ts.map +1 -1
  62. package/out/component.d.ts +50 -0
  63. package/out/component.d.ts.map +1 -0
  64. package/out/component.js +84 -0
  65. package/out/component.js.map +1 -0
  66. package/out/country/codes.d.ts +2 -2
  67. package/out/country/codes.d.ts.map +1 -1
  68. package/out/country/display-names.d.ts.map +1 -1
  69. package/out/country/display-names.js.map +1 -1
  70. package/out/country/index.d.ts +3 -2
  71. package/out/country/index.d.ts.map +1 -1
  72. package/out/country/index.js +3 -2
  73. package/out/country/index.js.map +1 -1
  74. package/out/country/{reference-data.d.ts → reference/data.d.ts} +1 -1
  75. package/out/country/reference/data.d.ts.map +1 -0
  76. package/out/country/{reference-data.js → reference/data.js} +1 -1
  77. package/out/country/reference/data.js.map +1 -0
  78. package/out/country/{reference.d.ts → reference/index.d.ts} +1 -1
  79. package/out/country/reference/index.d.ts.map +1 -0
  80. package/out/country/{reference.js → reference/index.js} +2 -2
  81. package/out/country/reference/index.js.map +1 -0
  82. package/out/country/region-languages.d.ts +25 -0
  83. package/out/country/region-languages.d.ts.map +1 -0
  84. package/out/country/region-languages.js +41 -0
  85. package/out/country/region-languages.js.map +1 -0
  86. package/out/de/bundesland.d.ts.map +1 -1
  87. package/out/de/postleitzahl.d.ts.map +1 -1
  88. package/out/de/street-type.d.ts.map +1 -1
  89. package/out/es/co-official-languages.d.ts +37 -0
  90. package/out/es/co-official-languages.d.ts.map +1 -0
  91. package/out/es/co-official-languages.js +61 -0
  92. package/out/es/co-official-languages.js.map +1 -0
  93. package/out/es/index.d.ts +1 -0
  94. package/out/es/index.d.ts.map +1 -1
  95. package/out/es/index.js +1 -0
  96. package/out/es/index.js.map +1 -1
  97. package/out/fr/departement.d.ts.map +1 -1
  98. package/out/fr/region.d.ts.map +1 -1
  99. package/out/gb/country.d.ts.map +1 -1
  100. package/out/gb/index.d.ts +2 -2
  101. package/out/gb/index.d.ts.map +1 -1
  102. package/out/gb/index.js +2 -2
  103. package/out/gb/index.js.map +1 -1
  104. package/out/gb/{postcode-area.d.ts → postcode/area.d.ts} +1 -1
  105. package/out/gb/postcode/area.d.ts.map +1 -0
  106. package/out/gb/{postcode-area.js → postcode/area.js} +1 -1
  107. package/out/gb/postcode/area.js.map +1 -0
  108. package/out/gb/{postcode.d.ts → postcode/index.d.ts} +1 -1
  109. package/out/gb/postcode/index.d.ts.map +1 -0
  110. package/out/gb/{postcode.js → postcode/index.js} +1 -1
  111. package/out/gb/postcode/index.js.map +1 -0
  112. package/out/gb/street-type.d.ts.map +1 -1
  113. package/out/index.d.ts +4 -2
  114. package/out/index.d.ts.map +1 -1
  115. package/out/index.js +4 -2
  116. package/out/index.js.map +1 -1
  117. package/out/jp/address-unit.d.ts.map +1 -1
  118. package/out/jp/index.d.ts +1 -0
  119. package/out/jp/index.d.ts.map +1 -1
  120. package/out/jp/index.js +1 -0
  121. package/out/jp/index.js.map +1 -1
  122. package/out/jp/municipality-register.d.ts +48 -0
  123. package/out/jp/municipality-register.d.ts.map +1 -0
  124. package/out/jp/municipality-register.js +41 -0
  125. package/out/jp/municipality-register.js.map +1 -0
  126. package/out/jp/prefecture.d.ts.map +1 -1
  127. package/out/kr/index.d.ts +10 -0
  128. package/out/kr/index.d.ts.map +1 -0
  129. package/out/kr/index.js +10 -0
  130. package/out/kr/index.js.map +1 -0
  131. package/out/kr/sigungu-register.d.ts +37 -0
  132. package/out/kr/sigungu-register.d.ts.map +1 -0
  133. package/out/kr/sigungu-register.js +283 -0
  134. package/out/kr/sigungu-register.js.map +1 -0
  135. package/out/level-semantics.d.ts.map +1 -1
  136. package/out/level-semantics.js.map +1 -1
  137. package/out/nz/delivery-service.d.ts.map +1 -1
  138. package/out/placetype-map.d.ts +83 -0
  139. package/out/placetype-map.d.ts.map +1 -0
  140. package/out/placetype-map.js +130 -0
  141. package/out/placetype-map.js.map +1 -0
  142. package/out/postcode/shapes.d.ts +47 -0
  143. package/out/postcode/shapes.d.ts.map +1 -0
  144. package/out/postcode/shapes.js +38 -0
  145. package/out/postcode/shapes.js.map +1 -0
  146. package/out/postcode/shapes.json +80 -0
  147. package/out/{postcode-systems.d.ts → postcode/systems.d.ts} +8 -1
  148. package/out/postcode/systems.d.ts.map +1 -0
  149. package/out/{postcode-systems.js → postcode/systems.js} +9 -2
  150. package/out/postcode/systems.js.map +1 -0
  151. package/out/register-completion.d.ts +14 -0
  152. package/out/register-completion.d.ts.map +1 -0
  153. package/out/register-completion.js +23 -0
  154. package/out/register-completion.js.map +1 -0
  155. package/out/us/floor-designator.d.ts.map +1 -1
  156. package/out/us/index.d.ts +2 -2
  157. package/out/us/index.js +2 -2
  158. package/out/us/military-address.d.ts.map +1 -1
  159. package/out/us/po-box.d.ts +6 -0
  160. package/out/us/po-box.d.ts.map +1 -1
  161. package/out/us/po-box.js +35 -10
  162. package/out/us/po-box.js.map +1 -1
  163. package/out/us/state.d.ts.map +1 -1
  164. package/out/us/{street-directional.d.ts → street/directional.d.ts} +1 -1
  165. package/out/us/street/directional.d.ts.map +1 -0
  166. package/out/us/{street-directional.js → street/directional.js} +2 -2
  167. package/out/us/street/directional.js.map +1 -0
  168. package/out/us/{street-suffix.d.ts → street/suffix.d.ts} +1 -1
  169. package/out/us/street/suffix.d.ts.map +1 -0
  170. package/out/us/{street-suffix.js → street/suffix.js} +2 -2
  171. package/out/us/street/suffix.js.map +1 -0
  172. package/out/us/zipcode.d.ts +1 -1
  173. package/out/us/zipcode.d.ts.map +1 -1
  174. package/package.json +120 -30
  175. package/lib/tools/build-country-surface-lexicon.ts +0 -177
  176. package/lib/tools/export-country-surfaces.ts +0 -48
  177. package/lib/tools/generate-country-population.ts +0 -142
  178. package/lib/tools/generate-country-reference.ts +0 -168
  179. package/lib/tools/generate-official-languages.ts +0 -210
  180. package/lib/tools/index.ts +0 -12
  181. package/out/address-system-conventions.d.ts.map +0 -1
  182. package/out/address-system-conventions.js.map +0 -1
  183. package/out/country/reference-data.d.ts.map +0 -1
  184. package/out/country/reference-data.js.map +0 -1
  185. package/out/country/reference.d.ts.map +0 -1
  186. package/out/country/reference.js.map +0 -1
  187. package/out/gb/postcode-area.d.ts.map +0 -1
  188. package/out/gb/postcode-area.js.map +0 -1
  189. package/out/gb/postcode.d.ts.map +0 -1
  190. package/out/gb/postcode.js.map +0 -1
  191. package/out/postcode-systems.d.ts.map +0 -1
  192. package/out/postcode-systems.js.map +0 -1
  193. package/out/tools/build-country-surface-lexicon.d.ts +0 -43
  194. package/out/tools/build-country-surface-lexicon.d.ts.map +0 -1
  195. package/out/tools/build-country-surface-lexicon.js +0 -149
  196. package/out/tools/build-country-surface-lexicon.js.map +0 -1
  197. package/out/tools/export-country-surfaces.d.ts +0 -15
  198. package/out/tools/export-country-surfaces.d.ts.map +0 -1
  199. package/out/tools/export-country-surfaces.js +0 -34
  200. package/out/tools/export-country-surfaces.js.map +0 -1
  201. package/out/tools/generate-country-population.d.ts +0 -42
  202. package/out/tools/generate-country-population.d.ts.map +0 -1
  203. package/out/tools/generate-country-population.js +0 -106
  204. package/out/tools/generate-country-population.js.map +0 -1
  205. package/out/tools/generate-country-reference.d.ts +0 -36
  206. package/out/tools/generate-country-reference.d.ts.map +0 -1
  207. package/out/tools/generate-country-reference.js +0 -107
  208. package/out/tools/generate-country-reference.js.map +0 -1
  209. package/out/tools/generate-official-languages.d.ts +0 -50
  210. package/out/tools/generate-official-languages.d.ts.map +0 -1
  211. package/out/tools/generate-official-languages.js +0 -149
  212. package/out/tools/generate-official-languages.js.map +0 -1
  213. package/out/tools/index.d.ts +0 -12
  214. package/out/tools/index.d.ts.map +0 -1
  215. package/out/tools/index.js +0 -12
  216. package/out/tools/index.js.map +0 -1
  217. package/out/us/street-directional.d.ts.map +0 -1
  218. package/out/us/street-directional.js.map +0 -1
  219. package/out/us/street-suffix.d.ts.map +0 -1
  220. package/out/us/street-suffix.js.map +0 -1
  221. /package/lib/country/{reference-data.ts → reference/data.ts} +0 -0
  222. /package/lib/gb/{postcode-area.ts → postcode/area.ts} +0 -0
  223. /package/lib/gb/{postcode.ts → postcode/index.ts} +0 -0
@@ -0,0 +1,213 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The per-country layout table. Each entry reads in the order it prints, so checking a country means looking at the
7
+ * shape of an address from there rather than at a nested call.
8
+ *
9
+ * THE LINE SKELETONS COME FROM libaddressinput, Google's address metadata, which this repository already ships at
10
+ * `packages/core/data/chromium-i18n/ssl-address/` (252 countries, Apache-2.0) and already has a refresh command for
11
+ * (`mailwoman dev download ssl-address`). Its `fmt` field is the print order — `%N%n%O%n%A%n%C, %S %Z` for the United
12
+ * States — and it carries the two things the OpenStreetMap-derived templates get wrong for the locales this project
13
+ * publishes weights for: Japan's postal mark and largest-first order, and China's unseparated admin run.
14
+ *
15
+ * WHAT THE DATASET DOES NOT CARRY, and what is therefore authored here:
16
+ *
17
+ * 1. **The `%A` expansion.** libaddressinput models the street address as ONE opaque field, because its consumers
18
+ * hold it as free text. This project's `ComponentTag` union splits it into a house number, the four street-family
19
+ * tags, a unit and the two intersection tags, so each address system says how those fill the line. Six orders
20
+ * cover the world: number-first or number-last, crossed with a space, a comma, or a line of its own. Measured
21
+ * across the OpenCage templates, 183 of the 211 countries that carry both slots take one of the two space forms.
22
+ * 2. **The line-join policy.** `%n` is a line break. Rendering it as `", "`, `" "` or `""` for single-line output is
23
+ * per-system, and it is the whole difference between a correct Japanese address and a reversed one.
24
+ *
25
+ * 3. **The country line.** `%R` is absent from nearly every `fmt`, because libaddressinput's consumers add the
26
+ * destination country themselves. It closes a small-first address and opens a large-first one, and it prints only
27
+ * when a caller supplies the name — an intra-country row carries none and prints none.
28
+ * 4. **The post-office box.** The dataset models no box at all; the street node carries it, on its own line directly
29
+ * above the street.
30
+ *
31
+ * Adding a country is therefore: transcribe its `fmt` skeleton, pick a street node, and close with the country line.
32
+ * A country whose real convention departs from the skeleton says so in place, with its source.
33
+ */
34
+
35
+ import {
36
+ addr,
37
+ numberFirstCommaStreet,
38
+ numberFirstStreet,
39
+ numberLastCommaStreet,
40
+ numberLastStreet,
41
+ SLOTS,
42
+ type AddressLayout,
43
+ } from "#address/layout"
44
+ import { GENERATED_ADDRESS_LAYOUTS } from "#address/layouts/generated"
45
+
46
+ const { attention, venue, house_number, street, dependent_locality, locality, subregion, region, postcode, country } =
47
+ SLOTS
48
+
49
+ /**
50
+ * The admin run below the prefecture in Japan, printed without separators. Japan's `fmt` carries no `%C` or `%D`, so
51
+ * everything below the prefecture rides the street-address field.
52
+ */
53
+ export const japaneseSubPrefecture = addr`${subregion}${locality}${dependent_locality}${house_number}`
54
+
55
+ /**
56
+ * China's street line: the name then the number, unseparated, below an admin run its `fmt` prints as `%S%C%D`.
57
+ */
58
+ export const chineseStreet = addr`${street}${house_number}`
59
+
60
+ /**
61
+ * How a system joins its lines for SINGLE-LINE output. Absent reads as `", "`, the anglophone default.
62
+ *
63
+ * The CJK entries are the reason this is per-system rather than a caller's argument: joining Japan's lines with a comma
64
+ * produces `1-9-1, 丸の内, 千代田区, 東京都 100-0005`, which is the romanized convention printed backwards.
65
+ */
66
+ export const LINE_JOINS: Readonly<Record<string, string>> = {
67
+ JP: " ",
68
+ CN: "",
69
+ TW: "",
70
+ KR: " ",
71
+ }
72
+
73
+ /**
74
+ * Address systems that print the largest unit first.
75
+ *
76
+ * It is a property of the system, not of the layout table, so a caller composing its own order — a gazetteer hierarchy
77
+ * string, say, which is a query rather than an address — reads it here instead of re-deriving it.
78
+ */
79
+ export const LARGEST_FIRST_SYSTEMS: ReadonlySet<string> = new Set(["JP", "CN", "TW", "KR"])
80
+
81
+ /**
82
+ * Whether the country named by `countryCode` prints the largest unit first.
83
+ */
84
+ export function isLargestFirstSystem(countryCode: string | null | undefined): boolean {
85
+ if (!countryCode) return false
86
+
87
+ return LARGEST_FIRST_SYSTEMS.has(countryCode.trim().toUpperCase())
88
+ }
89
+
90
+ /**
91
+ * Per-country layouts for the locales this project publishes weights for.
92
+ *
93
+ * Each is the country's libaddressinput `fmt` skeleton with `%A` expanded. The `fmt` is quoted beside it so the two can
94
+ * be compared without opening the dataset.
95
+ */
96
+ export const ADDRESS_LAYOUTS: Readonly<Record<string, AddressLayout>> = {
97
+ // %N%n%O%n%A%n%C, %S %Z
98
+ US: addr`${attention}
99
+ ${venue}
100
+ ${numberFirstStreet}
101
+ ${locality}, ${region} ${postcode}
102
+ ${country}`,
103
+
104
+ // %O%n%N%n%A%n%Z %C
105
+ FR: addr`${venue}
106
+ ${attention}
107
+ ${numberFirstStreet}
108
+ ${dependent_locality}
109
+ ${postcode} ${locality}
110
+ ${country}`,
111
+
112
+ // %N%n%O%n%A%n%C%n%Z
113
+ GB: addr`${attention}
114
+ ${venue}
115
+ ${numberFirstStreet}
116
+ ${dependent_locality}
117
+ ${locality}
118
+ ${postcode}
119
+ ${country}`,
120
+
121
+ // %N%n%O%n%A%n%Z %C
122
+ DE: addr`${attention}
123
+ ${venue}
124
+ ${numberLastStreet}
125
+ ${dependent_locality}
126
+ ${postcode} ${locality}
127
+ ${country}`,
128
+
129
+ // %N%n%O%n%A%n%Z %C %S — Spain writes the number after a comma: `Calle Mayor, 12`.
130
+ ES: addr`${attention}
131
+ ${venue}
132
+ ${numberLastCommaStreet}
133
+ ${dependent_locality}
134
+ ${postcode} ${locality} ${region}
135
+ ${country}`,
136
+
137
+ // %N%n%O%n%A%n%Z %C %S
138
+ IT: addr`${attention}
139
+ ${venue}
140
+ ${numberLastStreet}
141
+ ${dependent_locality}
142
+ ${postcode} ${locality} ${region}
143
+ ${country}`,
144
+
145
+ // %N%n%O%n%A%n%C %Z%n%S — India writes the number before a comma: `12, MG Road`.
146
+ IN: addr`${attention}
147
+ ${venue}
148
+ ${numberFirstCommaStreet}
149
+ ${dependent_locality}
150
+ ${locality} ${postcode}
151
+ ${region}
152
+ ${country}`,
153
+
154
+ // %N%n%O%n%A%n%D%n%C %Z
155
+ NZ: addr`${attention}
156
+ ${venue}
157
+ ${numberFirstStreet}
158
+ ${dependent_locality}
159
+ ${locality} ${postcode}
160
+ ${country}`,
161
+
162
+ // %O%n%N%n%A%n%C %S %Z
163
+ AU: addr`${venue}
164
+ ${attention}
165
+ ${numberFirstStreet}
166
+ ${dependent_locality}
167
+ ${locality} ${region} ${postcode}
168
+ ${country}`,
169
+
170
+ // 〒%Z%n%S%n%A%n%O%n%N, with the prefecture joined to the run below it.
171
+ //
172
+ // The dataset breaks the line between %S and %A because an envelope prints them on separate lines. Written on ONE
173
+ // line — which is what a geocoder query and a corpus row are — the whole admin run is unseparated and only the
174
+ // postal code takes a space: `〒100-0005 東京都千代田区丸の内1-9-1`. Keeping the dataset's break would put a space
175
+ // after the prefecture, which no Japanese address carries.
176
+ JP: addr`${country}
177
+ 〒${postcode}
178
+ ${region}${japaneseSubPrefecture}
179
+ ${venue}
180
+ ${attention}`,
181
+
182
+ // %Z%n%S%C%D%n%A%n%O%n%N — the admin run prints unseparated, which is what `LINE_JOINS.CN` carries.
183
+ CN: addr`${country}
184
+ ${postcode}
185
+ ${region}${locality}${dependent_locality}
186
+ ${chineseStreet}
187
+ ${venue}
188
+ ${attention}`,
189
+ }
190
+
191
+ /**
192
+ * The layout for `country`, or null when neither table names it.
193
+ *
194
+ * The hand-authored entries win: those are checked against real addresses on a board, where a generated skeleton is a
195
+ * transcription of a dataset. Null is a real answer — 55 of the 252 shipped country records carry no usable `fmt`, and
196
+ * a caller that renders nothing for one of those is reporting absence rather than inventing an order.
197
+ */
198
+ export function layoutForCountry(countryCode: string | null | undefined): AddressLayout | null {
199
+ if (!countryCode) return null
200
+
201
+ const code = countryCode.trim().toUpperCase()
202
+
203
+ return ADDRESS_LAYOUTS[code] ?? GENERATED_ADDRESS_LAYOUTS[code] ?? null
204
+ }
205
+
206
+ /**
207
+ * How the country named by `countryCode` joins its lines for single-line output.
208
+ */
209
+ export function lineJoinForCountry(countryCode: string | null | undefined): string {
210
+ if (!countryCode) return ", "
211
+
212
+ return LINE_JOINS[countryCode.trim().toUpperCase()] ?? ", "
213
+ }
@@ -0,0 +1,176 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Render a component dict through a per-country layout, keeping the tags.
7
+ *
8
+ * The formatter used to do three things in one function: order the components, join them into a string, and discard
9
+ * which tag produced which characters. A caller needing two of the three had to re-implement all three, which is why
10
+ * the same `filter(isPresent).join(…)` was hand-written in 28 files and why three corpus modules each hand-wrote
11
+ * per-country ordering the templates already encoded.
12
+ *
13
+ * This is the one render. `formatAddress` is the join over it, and the alignment a caller used to recover by
14
+ * searching the output string for each value is now a fact the render already holds: {@linkcode AddressRendering}
15
+ * says which tags it PLACED and names the ones it could not, where a substring search cannot tell a component the
16
+ * layout dropped from one whose value happens to sit inside another.
17
+ *
18
+ * ONE RULE, from `@mailwoman/codex/address-layout`: a node that renders nothing removes itself, and its connector
19
+ * goes with it. A connector between two slots needs a rendered slot on each side; a connector at a line's edge has
20
+ * one side, so it binds to the slot it touches. Adjacent survivors collapse to the first, so the layout's stronger
21
+ * separator wins — an absent region gives `New York, 10118`, which is what the engine this replaces produced.
22
+ */
23
+
24
+ import { isAlternation, isConnector, isLayout, isSlot, type AddressAtom, type AddressLayout } from "#address/layout"
25
+ import type { ComponentTag } from "#component"
26
+
27
+ /**
28
+ * A partial map of `ComponentTag` → string value — the canonical render input.
29
+ */
30
+ export type ComponentDict = Partial<Record<ComponentTag, string>>
31
+
32
+ /**
33
+ * One rendered piece. `tag` is null for a connector, which is what makes a rendering re-joinable at any separator
34
+ * without re-deriving which characters were structural.
35
+ */
36
+ export interface AddressPiece {
37
+ readonly tag: ComponentTag | null
38
+ readonly text: string
39
+ }
40
+
41
+ /**
42
+ * What a layout did with a dict.
43
+ */
44
+ export interface AddressRendering {
45
+ /**
46
+ * Every piece in print order, connectors included. Line breaks appear as a piece whose text is `"\n"`.
47
+ */
48
+ readonly pieces: readonly AddressPiece[]
49
+ /**
50
+ * The tags the layout printed.
51
+ */
52
+ readonly placed: readonly ComponentTag[]
53
+ /**
54
+ * Tags the dict carried a value for that the layout has no slot for — NAMED rather than silently dropped. France
55
+ * absorbing a region into its postcode line is the common case, and a caller aligning components against the output
56
+ * needs to know the difference between "not printed" and "not supplied".
57
+ */
58
+ readonly unplaced: readonly ComponentTag[]
59
+ }
60
+
61
+ /**
62
+ * Whether an atom produced any tagged piece, which is what a connector's neighbours are judged on.
63
+ */
64
+ function rendered(result: readonly AddressPiece[] | null): boolean {
65
+ return result !== null && result.some((piece) => piece.tag !== null)
66
+ }
67
+
68
+ function evaluateAtom(atom: AddressAtom, components: ComponentDict): readonly AddressPiece[] | null {
69
+ if (isSlot(atom)) {
70
+ const text = components[atom.tag]?.trim()
71
+
72
+ return text ? [{ tag: atom.tag, text }] : null
73
+ }
74
+
75
+ if (isAlternation(atom)) {
76
+ for (const alternative of atom.alternatives) {
77
+ const pieces = evaluateLines(alternative, components)
78
+
79
+ if (pieces.length) return pieces
80
+ }
81
+
82
+ return null
83
+ }
84
+
85
+ if (isLayout(atom)) {
86
+ const pieces = evaluateLines(atom, components)
87
+
88
+ return pieces.length ? pieces : null
89
+ }
90
+
91
+ return null
92
+ }
93
+
94
+ /**
95
+ * Which of a run of surviving connectors to print.
96
+ *
97
+ * A run forms when the slots between two connectors all render nothing, so what is left is several separators with no
98
+ * values between them. The STRONGEST wins: a connector carrying punctuation is a harder boundary than a space, and
99
+ * printing the space would join two values the layout meant to separate. `Calle Mayor, 12` keeps its comma when the
100
+ * street suffix is absent, and `New York, 10118` keeps its comma when the region is; the space forms of both would read
101
+ * as one value.
102
+ */
103
+ function strongestConnector(run: readonly string[]): string {
104
+ return run.find((text) => /\S/u.test(text)) ?? run[0]!
105
+ }
106
+
107
+ function evaluateLine(atoms: readonly AddressAtom[], components: ComponentDict): readonly AddressPiece[] {
108
+ const results = atoms.map((atom) => (isConnector(atom) ? null : evaluateAtom(atom, components)))
109
+ const out: AddressPiece[] = []
110
+ let pending: string[] = []
111
+
112
+ const flush = (): void => {
113
+ if (!pending.length) return
114
+
115
+ out.push({ tag: null, text: strongestConnector(pending) })
116
+ pending = []
117
+ }
118
+
119
+ for (const [index, atom] of atoms.entries()) {
120
+ if (isConnector(atom)) {
121
+ const left = results.slice(0, index)
122
+ const right = results.slice(index + 1)
123
+
124
+ // A connector at an edge binds to the one slot it touches; between slots it needs one on each side.
125
+ const survives = !left.length
126
+ ? rendered(results[index + 1] ?? null)
127
+ : !right.length
128
+ ? rendered(results[index - 1] ?? null)
129
+ : left.some(rendered) && right.some(rendered)
130
+
131
+ if (survives) {
132
+ pending.push(atom.connector)
133
+ }
134
+
135
+ continue
136
+ }
137
+
138
+ const pieces = results[index]
139
+
140
+ if (!pieces) continue
141
+
142
+ flush()
143
+ out.push(...pieces)
144
+ }
145
+
146
+ // Anything still pending trails the last value with nothing after it, so it separates nothing.
147
+ return out.some((piece) => piece.tag !== null) ? out : []
148
+ }
149
+
150
+ function evaluateLines(layout: AddressLayout, components: ComponentDict): readonly AddressPiece[] {
151
+ const lines = layout.lines.map((line) => evaluateLine(line, components)).filter((line) => line.length > 0)
152
+
153
+ return lines.flatMap((line, index) => (index > 0 ? [{ tag: null, text: "\n" }, ...line] : line))
154
+ }
155
+
156
+ /**
157
+ * Render `components` through `layout`, keeping the tags.
158
+ */
159
+ export function renderAddress(layout: AddressLayout, components: ComponentDict): AddressRendering {
160
+ const pieces = evaluateLines(layout, components)
161
+ const placed = [...new Set(pieces.map((piece) => piece.tag).filter((tag): tag is ComponentTag => tag !== null))]
162
+ const placedSet = new Set<ComponentTag>(placed)
163
+
164
+ const unplaced = (Object.keys(components) as ComponentTag[]).filter(
165
+ (tag) => Boolean(components[tag]?.trim()) && !placedSet.has(tag)
166
+ )
167
+
168
+ return { pieces, placed, unplaced }
169
+ }
170
+
171
+ /**
172
+ * Join a rendering into one string, replacing its line breaks with `separator`.
173
+ */
174
+ export function joinRendering(rendering: AddressRendering, separator = "\n"): string {
175
+ return rendering.pieces.map((piece) => (piece.tag === null && piece.text === "\n" ? separator : piece.text)).join("")
176
+ }
@@ -18,16 +18,17 @@
18
18
  * by the caller) — an undetected system parses exactly as before.
19
19
  */
20
20
 
21
+ import type { ComponentTag } from "#component"
21
22
  import { CODE_POSTAL_PATTERN } from "#fr/code-postal"
22
- import { UK_POSTCODE_PATTERN } from "#gb/postcode"
23
- import type { SystemCode } from "#postcode-systems"
23
+ import { UK_POSTCODE_PATTERN } from "#gb/postcode/index"
24
+ import type { SystemCode } from "#postcode/systems"
24
25
 
25
26
  export interface AddressSystemConventions {
26
27
  /**
27
- * Component tags that are NOT grammatical in this address system (names from the core `ComponentTag` union; codex
28
- * stays dependency-free so they are plain strings here).
28
+ * Component tags that are NOT grammatical in this address system. Typed against the union itself, so a tag that no
29
+ * longer exists is a compile error here rather than a rule that silently forbids nothing.
29
30
  */
30
- readonly forbiddenTags?: readonly string[]
31
+ readonly forbiddenTags?: readonly ComponentTag[]
31
32
  /**
32
33
  * The system's canonical postcode shape. A decoded postcode span that is a strict sub-match of a pattern-valid string
33
34
  * in the raw text is shape-INVALID for this system and eligible for the snap-only repair (extend/clip to the valid
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Canonical address component schema for the neural classifier pipeline.
7
+ *
8
+ * This file is the single source of truth for `ComponentTag`. Any change here requires (per #5 of
9
+ * the Mailwoman Neural plan):
10
+ *
11
+ * 1. A written rationale in the commit message.
12
+ * 2. A migration plan for corpus rows tagged with the prior schema.
13
+ * 3. A same-commit check that alignment, training, and inference code is updated to match.
14
+ *
15
+ * The legacy `Classifications` set in `../classification/scheme.ts` is kept independent on purpose:
16
+ * rule classifiers continue to emit those, the neural classifier emits these. The bridge between
17
+ * the two lives in the adapter layer (see #6).
18
+ */
19
+
20
+ /**
21
+ * The canonical address component tag union, ordered by phase and locale.
22
+ *
23
+ * - Universal (Phase 1): country, region, locality, dependent_locality, postcode, subregion
24
+ * - Street-level (Phase 2): house_number, street, street_prefix*, street_suffix, intersection_a/b, unit
25
+ * - Venue-level (Phase 3): venue, attention, po_box
26
+ * - FR-specific: cedex
27
+ * - JP-specific (ACTIVATED at CJK Phase 2, 2026-07-29 — the JP char model's 47-label head trains against them; the Latin
28
+ * model never emits them): prefecture, municipality, district, block, sub_block, building_number, building_name
29
+ */
30
+ export const COMPONENT_TAGS = [
31
+ // Universal
32
+ "country",
33
+ "region",
34
+ "locality",
35
+ "dependent_locality",
36
+ "postcode",
37
+ "subregion",
38
+ // Street-level
39
+ "house_number",
40
+ "street",
41
+ "street_prefix",
42
+ "street_prefix_particle",
43
+ "street_suffix",
44
+ "intersection_a",
45
+ "intersection_b",
46
+ "unit",
47
+ // Venue-level
48
+ "venue",
49
+ "attention",
50
+ "po_box",
51
+ // FR-specific
52
+ "cedex",
53
+ // JP-specific (ACTIVATED at CJK Phase 2 — the stage3-jp label set; Latin models never emit these)
54
+ "prefecture",
55
+ "municipality",
56
+ "district",
57
+ "block",
58
+ "sub_block",
59
+ "building_number",
60
+ "building_name",
61
+ // CN-specific (#2034): the organizational ladder China's rural addresses carry below the named settlement —
62
+ // state-farm sub-farms (分场), production brigades and teams (大队, 队, 生产队), XPCC regiments and companies (团, 连),
63
+ // villager groups (组). ONE contiguous span holds the whole ordinal chain (`三分场八队`); which rung each generic names
64
+ // is a deterministic reading of the suffix, not a label distinction, so the tag stays one and the split is derived.
65
+ // The named head unit (`孟定农场`, `新合`) is `dependent_locality`. Emitted by the CJK sibling model only.
66
+ "locality_unit",
67
+ ] as const
68
+
69
+ /**
70
+ * Union of every recognized address component tag.
71
+ */
72
+ export type ComponentTag = (typeof COMPONENT_TAGS)[number]
73
+
74
+ /**
75
+ * BIO-encoded label set: one `O` plus a `B-` / `I-` pair per tag.
76
+ *
77
+ * Used as the per-token output alphabet for the sequence-labeling neural model. Inference decodes a stream of these
78
+ * back into character-aligned `ClassificationProposal`s.
79
+ */
80
+ export const BIO_LABELS = ["O", ...COMPONENT_TAGS.flatMap((tag) => [`B-${tag}`, `I-${tag}`] as const)] as const
81
+
82
+ /**
83
+ * Union of every BIO label.
84
+ */
85
+ export type BIOLabel = (typeof BIO_LABELS)[number]
86
+
87
+ /**
88
+ * The street-name family in assembly order: prefix, particle, name, suffix. The order is part of the contract — a
89
+ * street surface is assembled by concatenating these tags' values in this order.
90
+ */
91
+ export const STREET_FAMILY_TAGS = [
92
+ "street_prefix",
93
+ "street_prefix_particle",
94
+ "street",
95
+ "street_suffix",
96
+ ] as const satisfies readonly ComponentTag[]
@@ -12,6 +12,7 @@ export * from "#country/display-names"
12
12
  export * from "#country/names"
13
13
  export * from "#country/official-languages"
14
14
  export * from "#country/population"
15
- export * from "#country/reference-data"
16
- export * from "#country/reference"
15
+ export * from "#country/reference/data"
16
+ export * from "#country/reference/index"
17
+ export * from "#country/region-languages"
17
18
  export * from "#country/subdivision"
@@ -11,7 +11,7 @@
11
11
 
12
12
  import type { AnnotationSet, Annotator } from "@mailwoman/annotations"
13
13
 
14
- import { COUNTRY_REFERENCE } from "#country/reference-data"
14
+ import { COUNTRY_REFERENCE } from "#country/reference/data"
15
15
 
16
16
  const REGIONAL_INDICATOR_BASE = 0x1_f1_e6
17
17
  const A_UPPER = "A".charCodeAt(0)
@@ -0,0 +1,46 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * The languages a REGION's addresses are written in: the country's official languages plus the region's own
7
+ * co-official ones, in ISO 639-3 (the code the Who's On First names table carries). This is the question a corpus
8
+ * extraction asks when it turns a region record into the surfaces a person types, and it differs from
9
+ * {@link isOfficialLanguage} in two ways: it answers per region rather than per country, and it refuses the
10
+ * country-wide `regional` list, whose languages are official somewhere in the country and not in the region at hand.
11
+ *
12
+ * Spain is the one country with a per-region table so far; every other country answers with its official languages
13
+ * alone, which for GB, FR, DE, IT, NL and PT is the language the source addresses are already written in.
14
+ */
15
+
16
+ import { OFFICIAL_LANGUAGES } from "#country/official-languages"
17
+ import { coOfficialLanguagesForProvince } from "#es/co-official-languages"
18
+
19
+ /**
20
+ * The length of an ISO 639-3 code, the spelling the Who's On First names table uses; the generated table lists each
21
+ * language under its ISO 639-1 spelling too, which is two letters.
22
+ */
23
+ const ALPHA3_LENGTH = 3
24
+
25
+ function isAlpha3(code: string): boolean {
26
+ return code.length === ALPHA3_LENGTH
27
+ }
28
+
29
+ /**
30
+ * The country's official languages in ISO 639-3, or empty for an unknown country.
31
+ */
32
+ export function officialLanguagesAlpha3(country: string): readonly string[] {
33
+ return (OFFICIAL_LANGUAGES[country.toUpperCase()]?.official ?? []).filter(isAlpha3)
34
+ }
35
+
36
+ /**
37
+ * The languages a region's addresses are written in: the country's official languages first, then the region's
38
+ * co-official ones. `regionOfficialName` is the region's name in the country's first official language, the key the
39
+ * per-country tables use.
40
+ */
41
+ export function regionLanguagesAlpha3(country: string, regionOfficialName: string): readonly string[] {
42
+ const official = officialLanguagesAlpha3(country)
43
+ const coOfficial = country.toUpperCase() === "ES" ? coOfficialLanguagesForProvince(regionOfficialName) : []
44
+
45
+ return [...official, ...coOfficial.filter((code) => !official.includes(code))]
46
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * Spain's co-official languages by province — the languages a person in that province writes an address in beside
7
+ * Castilian. The Constitution (art. 3.2) makes each community's Statute of Autonomy the instrument that declares a
8
+ * co-official language, so the table follows the statutes:
9
+ *
10
+ * - Catalan (`cat`): the Statute of Catalonia (2006, art. 6) for Barcelona, Girona, Lleida and Tarragona; the Statute
11
+ * of the Balearic Islands (2007, art. 4); and the Statute of the Valencian Community (2006, art. 6) for Alicante,
12
+ * Castellón and Valencia, whose Valencian the Who's On First names table files under `cat`.
13
+ * - Occitan (`oci`, Aranese): the Statute of Catalonia (2006, art. 6.5) makes it official throughout Catalonia.
14
+ * - Galician (`glg`): the Statute of Galicia (1981, art. 5) for A Coruña, Lugo, Ourense and Pontevedra.
15
+ * - Basque (`eus`): the Statute of the Basque Country (1979, art. 6) for Álava, Biscay and Gipuzkoa; the Foral Law
16
+ * 18/1986 of Navarre for its Basque-speaking zone.
17
+ *
18
+ * Asturian and Aragonese are protected by their statutes and not co-official, so Asturias, Huesca, Teruel and Zaragoza
19
+ * carry no entry. Ceuta, Melilla and the province-less territories carry none.
20
+ *
21
+ * WHY A TABLE AND NOT THE GAZETTEER. Who's On First stores a preferred name for a province in many languages, and for
22
+ * a language not spoken there the "preferred name" is often the AUTONOMOUS COMMUNITY's: Zamora's Catalan preferred
23
+ * name is `Castella i Lleó`, Seville's Asturian one is `Andalucía`, Ourense's Occitan one is `Galícia`. Reading every
24
+ * language's name as a surface of the province would teach those pairs. The statute says which languages a province's
25
+ * addresses are written in; this table carries that answer, keyed by the province's Castilian name as the gazetteer
26
+ * spells it in its `spa` preferred form.
27
+ */
28
+
29
+ /**
30
+ * ISO 639-3 codes of the co-official languages of each Spanish province that has one, keyed by the province's Castilian
31
+ * name (the `spa` preferred name in the Who's On First names table).
32
+ */
33
+ export const ES_PROVINCE_CO_OFFICIAL_LANGUAGES: ReadonlyMap<string, readonly string[]> = new Map([
34
+ // Catalonia: Catalan and Aranese Occitan (Statute of Catalonia 2006, art. 6).
35
+ ["Barcelona", ["cat", "oci"]],
36
+ ["Girona", ["cat", "oci"]],
37
+ ["Lleida", ["cat", "oci"]],
38
+ ["Tarragona", ["cat", "oci"]],
39
+ // Balearic Islands: Catalan (Statute 2007, art. 4).
40
+ ["Islas Baleares", ["cat"]],
41
+ // Valencian Community: Valencian, filed as `cat` (Statute 2006, art. 6).
42
+ ["Alicante", ["cat"]],
43
+ ["Castellón", ["cat"]],
44
+ ["Valencia", ["cat"]],
45
+ // Galicia: Galician (Statute 1981, art. 5).
46
+ ["La Coruña", ["glg"]],
47
+ ["Lugo", ["glg"]],
48
+ ["Orense", ["glg"]],
49
+ ["Pontevedra", ["glg"]],
50
+ // Basque Country: Basque (Statute 1979, art. 6); Navarre: Basque in its Basque-speaking zone (Foral Law 18/1986).
51
+ ["Álava", ["eus"]],
52
+ ["Vizcaya", ["eus"]],
53
+ ["Guipúzcoa", ["eus"]],
54
+ ["Navarra", ["eus"]],
55
+ ])
56
+
57
+ /**
58
+ * The co-official languages of a Spanish province, by its Castilian name; empty for a province with none.
59
+ */
60
+ export function coOfficialLanguagesForProvince(castilianName: string): readonly string[] {
61
+ return ES_PROVINCE_CO_OFFICIAL_LANGUAGES.get(castilianName.trim()) ?? []
62
+ }
package/lib/es/index.ts CHANGED
@@ -6,4 +6,5 @@
6
6
  * Spanish (ES) address-system reference data.
7
7
  */
8
8
 
9
+ export * from "#es/co-official-languages"
9
10
  export * from "#es/codigo-postal"
package/lib/gb/index.ts CHANGED
@@ -9,6 +9,6 @@
9
9
  */
10
10
 
11
11
  export * from "#gb/country"
12
- export * from "#gb/postcode-area"
13
- export * from "#gb/postcode"
12
+ export * from "#gb/postcode/area"
13
+ export * from "#gb/postcode/index"
14
14
  export * from "#gb/street-type"
package/lib/index.ts CHANGED
@@ -25,9 +25,11 @@ export {
25
25
  ADDRESS_SYSTEM_CONVENTIONS,
26
26
  conventionsForSystem,
27
27
  type AddressSystemConventions,
28
- } from "#address-system-conventions"
28
+ } from "#address/system-conventions"
29
29
 
30
+ export * from "#component"
30
31
  export * from "#normalize"
32
+ export * from "#placetype-map"
31
33
  export * as au from "#au/index"
32
34
  export * as ca from "#ca/index"
33
35
  export * as de from "#de/index"
@@ -45,6 +47,6 @@ export {
45
47
  SYSTEM_CODES,
46
48
  type SystemCode,
47
49
  UNIT_GRADE_POSTCODE,
48
- } from "#postcode-systems"
50
+ } from "#postcode/systems"
49
51
 
50
52
  export * as us from "#us/index"