@mailwoman/corpus 10.5.0 → 10.7.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 (232) hide show
  1. package/data/PROVENANCE.md +24 -0
  2. package/data/address-source-register.json +18948 -8757
  3. package/data/corpus-source-inputs.json +4756 -0
  4. package/data/license-decisions.json +7260 -0
  5. package/data/licenses/whosonfirst-licenses.md +3 -2
  6. package/data/reviewed-ve-postcode-tuples.json +5 -72
  7. package/data/source-additions.json +2450 -0
  8. package/data/source-resolutions.json +87 -0
  9. package/data/training-manifests/v0.7.10-gb-county.effective.json +2335 -0
  10. package/data/training-manifests/v0.7.10-gb-county.json +1132 -0
  11. package/data/training-manifests/v0.7.2-address-systems.effective.json +3 -2
  12. package/data/training-manifests/v0.7.7-no-ppd.json +550 -0
  13. package/data/training-manifests/v0.7.8-rights-clean.json +538 -0
  14. package/data/training-manifests/v0.7.9-country-gap.json +1132 -0
  15. package/data/us-situs-sources.json +128 -0
  16. package/lib/adapters/cordis/adapter.ts +1 -1
  17. package/lib/adapters/libpostal-dictionary.ts +23 -3
  18. package/lib/adapters/osm/adapter.ts +42 -11
  19. package/lib/adapters.ts +14 -0
  20. package/lib/az/adapters/opendata/adapter.ts +389 -0
  21. package/lib/bs/adapters/gbpa/adapter.ts +248 -0
  22. package/lib/build/types.ts +16 -0
  23. package/lib/build.ts +21 -0
  24. package/lib/ee/adapters/ads/adapter.ts +18 -0
  25. package/lib/fo/adapters/adressur/adapter.ts +242 -0
  26. package/lib/gb/adapters/os-open/adapter.ts +14 -2
  27. package/lib/gb/county.ts +59 -0
  28. package/lib/gb/os-open/{open-names.ts → open/names.ts} +41 -0
  29. package/lib/gu/adapters/bsp/adapter.ts +434 -0
  30. package/lib/im/adapters/land-registry/adapter.ts +340 -0
  31. package/lib/international/recipes/locale.ts +164 -19
  32. package/lib/li/adapters/llv/adapter.ts +246 -0
  33. package/lib/recipes/bare/postcode.ts +10 -34
  34. package/lib/recipes/country-balanced.ts +95 -78
  35. package/lib/recipes/intersection.ts +22 -30
  36. package/lib/recipes/po/box/cedex/recipe.ts +8 -5
  37. package/lib/recipes/po/box/cedex/sources.ts +2 -23
  38. package/lib/recipes/scaffold.ts +28 -3
  39. package/lib/recipes/sources.ts +112 -1
  40. package/lib/recipes/street/affix.ts +17 -29
  41. package/lib/recipes/sub/venue/context.ts +33 -9
  42. package/lib/recipes/sub/venue/sources.ts +54 -13
  43. package/lib/recipes/sub/venue.ts +68 -23
  44. package/lib/recipes/unit.ts +20 -7
  45. package/lib/registers.ts +33 -0
  46. package/lib/source-register/corpus-inputs.ts +528 -0
  47. package/lib/source-register/effective-manifest.ts +37 -5
  48. package/lib/source-register/types.ts +20 -0
  49. package/lib/south-asia/recipes/register.ts +1 -1
  50. package/lib/tools/overlay/manifest.ts +109 -1
  51. package/lib/tools/source-register/build.ts +47 -1
  52. package/lib/tools/variety-sample.ts +633 -0
  53. package/lib/us/adapters/usgov/nad/adapter.ts +39 -10
  54. package/lib/us/recipes/skeleton-sources.ts +156 -0
  55. package/lib/us/tools/nad-skeletons.ts +395 -0
  56. package/lib/uy/adapters/ide/adapter.ts +275 -0
  57. package/lib/ve/recipes/reviewed-postcode-tail.ts +29 -6
  58. package/out/adapters/cordis/adapter.d.ts +1 -1
  59. package/out/adapters/cordis/adapter.js +1 -1
  60. package/out/adapters/libpostal-dictionary.d.ts +9 -0
  61. package/out/adapters/libpostal-dictionary.d.ts.map +1 -1
  62. package/out/adapters/libpostal-dictionary.js +19 -3
  63. package/out/adapters/libpostal-dictionary.js.map +1 -1
  64. package/out/adapters/osm/adapter.d.ts +12 -1
  65. package/out/adapters/osm/adapter.d.ts.map +1 -1
  66. package/out/adapters/osm/adapter.js +30 -13
  67. package/out/adapters/osm/adapter.js.map +1 -1
  68. package/out/adapters.d.ts.map +1 -1
  69. package/out/adapters.js +14 -0
  70. package/out/adapters.js.map +1 -1
  71. package/out/az/adapters/opendata/adapter.d.ts +88 -0
  72. package/out/az/adapters/opendata/adapter.d.ts.map +1 -0
  73. package/out/az/adapters/opendata/adapter.js +282 -0
  74. package/out/az/adapters/opendata/adapter.js.map +1 -0
  75. package/out/bs/adapters/gbpa/adapter.d.ts +64 -0
  76. package/out/bs/adapters/gbpa/adapter.d.ts.map +1 -0
  77. package/out/bs/adapters/gbpa/adapter.js +187 -0
  78. package/out/bs/adapters/gbpa/adapter.js.map +1 -0
  79. package/out/build/types.d.ts +15 -0
  80. package/out/build/types.d.ts.map +1 -1
  81. package/out/build/types.js.map +1 -1
  82. package/out/build.d.ts.map +1 -1
  83. package/out/build.js +17 -0
  84. package/out/build.js.map +1 -1
  85. package/out/ee/adapters/ads/adapter.d.ts +6 -0
  86. package/out/ee/adapters/ads/adapter.d.ts.map +1 -1
  87. package/out/ee/adapters/ads/adapter.js +16 -0
  88. package/out/ee/adapters/ads/adapter.js.map +1 -1
  89. package/out/fo/adapters/adressur/adapter.d.ts +63 -0
  90. package/out/fo/adapters/adressur/adapter.d.ts.map +1 -0
  91. package/out/fo/adapters/adressur/adapter.js +177 -0
  92. package/out/fo/adapters/adressur/adapter.js.map +1 -0
  93. package/out/gb/adapters/os-open/adapter.d.ts +10 -2
  94. package/out/gb/adapters/os-open/adapter.d.ts.map +1 -1
  95. package/out/gb/adapters/os-open/adapter.js +13 -2
  96. package/out/gb/adapters/os-open/adapter.js.map +1 -1
  97. package/out/gb/county.d.ts +30 -0
  98. package/out/gb/county.d.ts.map +1 -0
  99. package/out/gb/county.js +51 -0
  100. package/out/gb/county.js.map +1 -0
  101. package/out/gb/os-open/{open-names.d.ts → open/names.d.ts} +18 -1
  102. package/out/gb/os-open/open/names.d.ts.map +1 -0
  103. package/out/gb/os-open/{open-names.js → open/names.js} +20 -1
  104. package/out/gb/os-open/open/names.js.map +1 -0
  105. package/out/gb/os-open/{open-roads.d.ts → open/roads.d.ts} +1 -1
  106. package/out/gb/os-open/open/roads.d.ts.map +1 -0
  107. package/out/gb/os-open/{open-roads.js → open/roads.js} +1 -1
  108. package/out/gb/os-open/open/roads.js.map +1 -0
  109. package/out/gu/adapters/bsp/adapter.d.ts +83 -0
  110. package/out/gu/adapters/bsp/adapter.d.ts.map +1 -0
  111. package/out/gu/adapters/bsp/adapter.js +346 -0
  112. package/out/gu/adapters/bsp/adapter.js.map +1 -0
  113. package/out/im/adapters/land-registry/adapter.d.ts +68 -0
  114. package/out/im/adapters/land-registry/adapter.d.ts.map +1 -0
  115. package/out/im/adapters/land-registry/adapter.js +261 -0
  116. package/out/im/adapters/land-registry/adapter.js.map +1 -0
  117. package/out/international/recipes/locale.d.ts +24 -4
  118. package/out/international/recipes/locale.d.ts.map +1 -1
  119. package/out/international/recipes/locale.js +107 -15
  120. package/out/international/recipes/locale.js.map +1 -1
  121. package/out/li/adapters/llv/adapter.d.ts +69 -0
  122. package/out/li/adapters/llv/adapter.d.ts.map +1 -0
  123. package/out/li/adapters/llv/adapter.js +181 -0
  124. package/out/li/adapters/llv/adapter.js.map +1 -0
  125. package/out/recipes/bare/postcode.d.ts.map +1 -1
  126. package/out/recipes/bare/postcode.js +9 -30
  127. package/out/recipes/bare/postcode.js.map +1 -1
  128. package/out/recipes/country-balanced.d.ts.map +1 -1
  129. package/out/recipes/country-balanced.js +82 -73
  130. package/out/recipes/country-balanced.js.map +1 -1
  131. package/out/recipes/intersection.d.ts +3 -2
  132. package/out/recipes/intersection.d.ts.map +1 -1
  133. package/out/recipes/intersection.js +20 -26
  134. package/out/recipes/intersection.js.map +1 -1
  135. package/out/recipes/po/box/cedex/recipe.d.ts.map +1 -1
  136. package/out/recipes/po/box/cedex/recipe.js +5 -4
  137. package/out/recipes/po/box/cedex/recipe.js.map +1 -1
  138. package/out/recipes/po/box/cedex/sources.d.ts +2 -45
  139. package/out/recipes/po/box/cedex/sources.d.ts.map +1 -1
  140. package/out/recipes/po/box/cedex/sources.js +0 -20
  141. package/out/recipes/po/box/cedex/sources.js.map +1 -1
  142. package/out/recipes/scaffold.d.ts +15 -2
  143. package/out/recipes/scaffold.d.ts.map +1 -1
  144. package/out/recipes/scaffold.js +21 -1
  145. package/out/recipes/scaffold.js.map +1 -1
  146. package/out/recipes/sources.d.ts.map +1 -1
  147. package/out/recipes/sources.js +112 -1
  148. package/out/recipes/sources.js.map +1 -1
  149. package/out/recipes/street/affix.d.ts.map +1 -1
  150. package/out/recipes/street/affix.js +8 -20
  151. package/out/recipes/street/affix.js.map +1 -1
  152. package/out/recipes/sub/venue/context.d.ts +14 -3
  153. package/out/recipes/sub/venue/context.d.ts.map +1 -1
  154. package/out/recipes/sub/venue/context.js +29 -9
  155. package/out/recipes/sub/venue/context.js.map +1 -1
  156. package/out/recipes/sub/venue/sources.d.ts +41 -8
  157. package/out/recipes/sub/venue/sources.d.ts.map +1 -1
  158. package/out/recipes/sub/venue/sources.js +26 -7
  159. package/out/recipes/sub/venue/sources.js.map +1 -1
  160. package/out/recipes/sub/venue.d.ts.map +1 -1
  161. package/out/recipes/sub/venue.js +35 -14
  162. package/out/recipes/sub/venue.js.map +1 -1
  163. package/out/recipes/unit.d.ts +4 -0
  164. package/out/recipes/unit.d.ts.map +1 -1
  165. package/out/recipes/unit.js +7 -6
  166. package/out/recipes/unit.js.map +1 -1
  167. package/out/registers.d.ts +33 -0
  168. package/out/registers.d.ts.map +1 -1
  169. package/out/registers.js +33 -0
  170. package/out/registers.js.map +1 -1
  171. package/out/source-register/corpus-inputs.d.ts +240 -0
  172. package/out/source-register/corpus-inputs.d.ts.map +1 -0
  173. package/out/source-register/corpus-inputs.js +275 -0
  174. package/out/source-register/corpus-inputs.js.map +1 -0
  175. package/out/source-register/effective-manifest.d.ts +2 -0
  176. package/out/source-register/effective-manifest.d.ts.map +1 -1
  177. package/out/source-register/effective-manifest.js +33 -4
  178. package/out/source-register/effective-manifest.js.map +1 -1
  179. package/out/source-register/types.d.ts +21 -1
  180. package/out/source-register/types.d.ts.map +1 -1
  181. package/out/source-register/types.js +20 -0
  182. package/out/source-register/types.js.map +1 -1
  183. package/out/south-asia/recipes/register.js +1 -1
  184. package/out/south-asia/recipes/register.js.map +1 -1
  185. package/out/tools/overlay/manifest.d.ts +7 -0
  186. package/out/tools/overlay/manifest.d.ts.map +1 -1
  187. package/out/tools/overlay/manifest.js +72 -1
  188. package/out/tools/overlay/manifest.js.map +1 -1
  189. package/out/tools/source-register/build.d.ts +9 -0
  190. package/out/tools/source-register/build.d.ts.map +1 -1
  191. package/out/tools/source-register/build.js +25 -3
  192. package/out/tools/source-register/build.js.map +1 -1
  193. package/out/tools/variety-sample.d.ts +196 -0
  194. package/out/tools/variety-sample.d.ts.map +1 -0
  195. package/out/tools/variety-sample.js +417 -0
  196. package/out/tools/variety-sample.js.map +1 -0
  197. package/out/us/adapters/usgov/nad/adapter.d.ts +80 -0
  198. package/out/us/adapters/usgov/nad/adapter.d.ts.map +1 -1
  199. package/out/us/adapters/usgov/nad/adapter.js +27 -8
  200. package/out/us/adapters/usgov/nad/adapter.js.map +1 -1
  201. package/out/us/recipes/skeleton-sources.d.ts +92 -0
  202. package/out/us/recipes/skeleton-sources.d.ts.map +1 -0
  203. package/out/us/recipes/skeleton-sources.js +121 -0
  204. package/out/us/recipes/skeleton-sources.js.map +1 -0
  205. package/out/us/tools/nad-skeletons.d.ts +120 -0
  206. package/out/us/tools/nad-skeletons.d.ts.map +1 -0
  207. package/out/us/tools/nad-skeletons.js +262 -0
  208. package/out/us/tools/nad-skeletons.js.map +1 -0
  209. package/out/uy/adapters/ide/adapter.d.ts +64 -0
  210. package/out/uy/adapters/ide/adapter.d.ts.map +1 -0
  211. package/out/uy/adapters/ide/adapter.js +206 -0
  212. package/out/uy/adapters/ide/adapter.js.map +1 -0
  213. package/out/ve/recipes/reviewed-postcode-tail.d.ts +6 -2
  214. package/out/ve/recipes/reviewed-postcode-tail.d.ts.map +1 -1
  215. package/out/ve/recipes/reviewed-postcode-tail.js +16 -5
  216. package/out/ve/recipes/reviewed-postcode-tail.js.map +1 -1
  217. package/package.json +14 -14
  218. package/lib/recipes/unit/sources.ts +0 -44
  219. package/lib/tools/fetch/ppd.ts +0 -121
  220. package/out/gb/os-open/open-names.d.ts.map +0 -1
  221. package/out/gb/os-open/open-names.js.map +0 -1
  222. package/out/gb/os-open/open-roads.d.ts.map +0 -1
  223. package/out/gb/os-open/open-roads.js.map +0 -1
  224. package/out/recipes/unit/sources.d.ts +0 -28
  225. package/out/recipes/unit/sources.d.ts.map +0 -1
  226. package/out/recipes/unit/sources.js +0 -30
  227. package/out/recipes/unit/sources.js.map +0 -1
  228. package/out/tools/fetch/ppd.d.ts +0 -15
  229. package/out/tools/fetch/ppd.d.ts.map +0 -1
  230. package/out/tools/fetch/ppd.js +0 -86
  231. package/out/tools/fetch/ppd.js.map +0 -1
  232. /package/lib/gb/os-open/{open-roads.ts → open/roads.ts} +0 -0
@@ -0,0 +1,434 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ *
6
+ * `bsp-gu`: the address points of Guam, which the Bureau of Statistics & Plans GIS publishes on the
7
+ * Government of Guam GISWEB Portal as the ArcGIS layer `Address_Point_(Public_View)`.
8
+ *
9
+ * The input is the layer's GeoJSON query pages, one file or a directory of them. The adapter reads
10
+ * `HOUSE_NUM` as the house number, `PRE_DIR` as the street prefix, `STREETNAME` as the street (or the
11
+ * assembled `FullRdNm` when `STREETNAME` is blank), `STR_TYPE` as the street suffix, `UNIT` as the unit
12
+ * and `CITY` (or `PLACENAME` when `CITY` is blank) as the locality. Route roads read `ROUTE 10` with the
13
+ * type `HWY`, which is how the layer's own `FullRdNm` writes them.
14
+ *
15
+ * The layer carries no postcode: its NG911 `Post_Code` column is blank on every feature.
16
+ *
17
+ * `HOUSE_NUM` sometimes pairs the number with a building-permit number (`120 / B19000483`); the number is
18
+ * kept and the permit dropped. `CITY` writes Guam's villages with Chamorro and English spellings
19
+ * (`HAGÅTÑA`, `HAGATNA`; `INARAJAN`, `INALAHAN`) and with typing slips (`BARRIGDAA`, `MANGIL.AO`), so a
20
+ * value is kept only when each of its parts is a village, or a community the villages contain,
21
+ * listed in {@link GUAM_PLACE_NAMES}. A road written `(OFF OF) ROUTE 4A HWY` locates a point near a road
22
+ * rather than on it, and a street naming two roads (`AGUILAR RD / RT 17`) states no single street, so both
23
+ * are refused.
24
+ *
25
+ * The layer states no license. Under the United States position in `packages/corpus/lib/us/AGENTS.md`
26
+ * (Feist), a US public body's address file published without terms restricting its use is elected,
27
+ * and the rows carry the label the US public-body adapters use.
28
+ */
29
+
30
+ import { formatAddressRow } from "@mailwoman/codex/address/format"
31
+ import { foldToken } from "@mailwoman/codex/normalize"
32
+ import { readLocalTextFile } from "@mailwoman/core/fs/readers"
33
+ import { tryStat } from "@mailwoman/core/fs/readers/stat"
34
+ import { parseJSONStrict } from "@mailwoman/core/json"
35
+ import { resolvePath } from "path-ts"
36
+ import { Globerator } from "spliterator/node/fs"
37
+
38
+ import { UnsupportedCountryError } from "#adapters/errors"
39
+ import { stableSourceIDFromParts } from "#adapters/source-id"
40
+ import { SourceRegister } from "#registers"
41
+ import {
42
+ AddressRole,
43
+ type AdapterOptions,
44
+ type CanonicalRow,
45
+ type CorpusAdapter,
46
+ countDropped,
47
+ SurfaceOrigin,
48
+ } from "#types"
49
+
50
+ /**
51
+ * Registry id for this adapter, stamped into every row it emits.
52
+ */
53
+ export const BSP_GU_ADAPTER_ID = "bsp-gu"
54
+
55
+ /**
56
+ * The label every row carries, matching the US public-body adapters elected under the Feist reading.
57
+ */
58
+ export const BSP_GU_LICENSE = "Public Domain"
59
+
60
+ /**
61
+ * Every jurisdiction this adapter emits, checked against a caller's `--country`.
62
+ */
63
+ export const BSP_GU_COUNTRIES: readonly string[] = ["GU"]
64
+
65
+ /**
66
+ * The feature properties the adapter reads from the layer.
67
+ */
68
+ export interface GuamAddressPointRecord {
69
+ FID?: number | null
70
+ HOUSE_NUM?: string | null
71
+ PRE_DIR?: string | null
72
+ STREETNAME?: string | null
73
+ STR_TYPE?: string | null
74
+ FullRdNm?: string | null
75
+ UNIT?: string | null
76
+ CITY?: string | null
77
+ PLACENAME?: string | null
78
+ }
79
+
80
+ interface GuamFeatureCollection {
81
+ features?: Array<{
82
+ properties?: GuamAddressPointRecord | null
83
+ geometry?: { coordinates?: readonly number[] } | null
84
+ }>
85
+ }
86
+
87
+ /**
88
+ * A value with its whitespace collapsed, or null when the layer leaves it blank.
89
+ * The layer writes an absent string as a single space.
90
+ */
91
+ function present(value: string | number | null | undefined): string | null {
92
+ const collapsed = value == null ? "" : String(value).replaceAll(/\s+/gu, " ").trim()
93
+
94
+ return collapsed || null
95
+ }
96
+
97
+ /**
98
+ * Upper-cased, with diacritics removed, so `HAGÅTÑA` and `Hagatna` compare equal.
99
+ */
100
+ function upperFold(value: string): string {
101
+ return foldToken(value).toUpperCase()
102
+ }
103
+
104
+ /**
105
+ * Guam's nineteen villages under each name the layer writes for them,
106
+ * and the communities within a village that `CITY` names on its own
107
+ * (Tumon and Harmon in Tamuning, Ipan in Talofofo, Paasan in Agana Heights, Anigua in Hagåtña), folded.
108
+ */
109
+ export const GUAM_PLACE_NAMES: ReadonlySet<string> = new Set([
110
+ "AGANA HEIGHTS",
111
+ "PAASAN",
112
+ "AGAT",
113
+ "HAGAT",
114
+ "ASAN",
115
+ "MAINA",
116
+ "ASAN MAINA",
117
+ "BARRIGADA",
118
+ "BARRIGADA HEIGHTS",
119
+ "CHALAN PAGO",
120
+ "ORDOT",
121
+ "CHALAN PAGO ORDOT",
122
+ "DEDEDO",
123
+ "HAGATNA",
124
+ "AGANA",
125
+ "ANIGUA",
126
+ "INARAJAN",
127
+ "INALAHAN",
128
+ "INALAJAN",
129
+ "MANGILAO",
130
+ "MERIZO",
131
+ "MALESSO",
132
+ "MONGMONG",
133
+ "TOTO",
134
+ "MAITE",
135
+ "MONGMONG TOTO MAITE",
136
+ "PITI",
137
+ "SANTA RITA",
138
+ "SINAJANA",
139
+ "TALOFOFO",
140
+ "IPAN",
141
+ "TAMUNING",
142
+ "TUMON",
143
+ "HARMON",
144
+ "UMATAC",
145
+ "HUMATAC",
146
+ "YIGO",
147
+ "YONA",
148
+ ])
149
+
150
+ /**
151
+ * Whether every part of a place value, split at commas, slashes, hyphens and parentheses,
152
+ * is a listed place: `CHALAN PAGO - ORDOT` and `AGANA HEIGHTS (PAASAN)` are, `RADIO, BARRIGADA` is not.
153
+ */
154
+ export function isGuamPlaceName(value: string): boolean {
155
+ const parts = upperFold(value)
156
+ .split(/[,/()-]/u)
157
+ .map((part) => part.trim())
158
+ .filter((part) => part.length > 0)
159
+
160
+ return parts.length > 0 && parts.every((part) => GUAM_PLACE_NAMES.has(part))
161
+ }
162
+
163
+ /**
164
+ * A house number: digits, an optional hyphenated half, and an optional letter written
165
+ * against the number or after a space (`120`, `99-12`, `231A`, `155 A`).
166
+ */
167
+ const HOUSE_NUMBER = /^\d+(?:-\d+)?(?: ?[A-Z])?$/u
168
+
169
+ /**
170
+ * A Guam building-permit number, written beside a house number in `HOUSE_NUM` (`B19000483`, `B22-450`).
171
+ */
172
+ const PERMIT_NUMBER = /^[A-Z]{1,2}\d{2}-?\d{3,}$/iu
173
+
174
+ /**
175
+ * The house number `HOUSE_NUM` states, with a building-permit number written beside
176
+ * it removed, or null when the value is not a house number.
177
+ */
178
+ export function guamHouseNumber(value: string): string | null {
179
+ if (HOUSE_NUMBER.test(value)) return value
180
+
181
+ const parts = value
182
+ .split("/")
183
+ .map((part) => part.trim())
184
+ .filter((part) => part.length > 0)
185
+
186
+ const numbers = parts.filter((part) => HOUSE_NUMBER.test(part))
187
+
188
+ if (numbers.length === 1 && parts.every((part) => numbers.includes(part) || PERMIT_NUMBER.test(part))) {
189
+ return numbers[0]!
190
+ }
191
+
192
+ return null
193
+ }
194
+
195
+ /**
196
+ * The street types `STR_TYPE` writes, as the layer abbreviates or spells them.
197
+ */
198
+ const STREET_TYPES: ReadonlySet<string> = new Set([
199
+ "ST",
200
+ "STREET",
201
+ "HWY",
202
+ "HIGHWAY",
203
+ "RD",
204
+ "ROAD",
205
+ "CT",
206
+ "CRT",
207
+ "COURT",
208
+ "DR",
209
+ "DRIVE",
210
+ "LN",
211
+ "LANE",
212
+ "AVE",
213
+ "AVENUE",
214
+ "LOOP",
215
+ "LP",
216
+ "WAY",
217
+ "WY",
218
+ "CIR",
219
+ "CIRCLE",
220
+ "BLVD",
221
+ "BOULEVARD",
222
+ "PL",
223
+ ])
224
+
225
+ const DIRECTIONS: ReadonlySet<string> = new Set(["N", "S", "E", "W"])
226
+
227
+ /**
228
+ * Maps one record onto the components the corpus asserts, or returns null when the row is refused.
229
+ *
230
+ * Each refusal and each dropped component increments its reason in `dropped`.
231
+ */
232
+ export function componentsForGuamRecord(
233
+ record: GuamAddressPointRecord,
234
+ dropped?: Pick<AdapterOptions, "dropped">
235
+ ): CanonicalRow["components"] | null {
236
+ const tally = dropped ?? {}
237
+ const written = present(record.HOUSE_NUM)
238
+
239
+ if (!written) {
240
+ countDropped(tally, "row:no-house-number")
241
+
242
+ return null
243
+ }
244
+
245
+ const houseNumber = guamHouseNumber(written)
246
+
247
+ if (!houseNumber) {
248
+ countDropped(tally, "row:house-number-not-a-number")
249
+
250
+ return null
251
+ }
252
+
253
+ const name = present(record.STREETNAME)
254
+ const street = name ?? present(record.FullRdNm)
255
+
256
+ if (!street) {
257
+ countDropped(tally, "row:no-street")
258
+
259
+ return null
260
+ }
261
+
262
+ if (/^\(?OFF OF\b/iu.test(street)) {
263
+ countDropped(tally, "row:street-off-of")
264
+
265
+ return null
266
+ }
267
+
268
+ if (/[/()]/u.test(street)) {
269
+ countDropped(tally, "row:street-names-two-roads")
270
+
271
+ return null
272
+ }
273
+
274
+ const components: CanonicalRow["components"] = { house_number: houseNumber, street }
275
+
276
+ // `FullRdNm` already holds the direction and the type, so they are read beside `STREETNAME` only.
277
+ if (name) {
278
+ const direction = present(record.PRE_DIR)?.toUpperCase()
279
+
280
+ if (direction && DIRECTIONS.has(direction)) {
281
+ if (upperFold(name).startsWith(`${direction} `)) {
282
+ countDropped(tally, "component:street_prefix:repeats-street")
283
+ } else {
284
+ components.street_prefix = direction
285
+ }
286
+ } else if (direction) {
287
+ countDropped(tally, "component:street_prefix:not-a-direction")
288
+ }
289
+
290
+ const type = present(record.STR_TYPE)?.toUpperCase().replace(/\.$/u, "")
291
+
292
+ if (type && STREET_TYPES.has(type)) {
293
+ if (upperFold(name).endsWith(` ${type}`)) {
294
+ countDropped(tally, "component:street_suffix:repeats-street")
295
+ } else {
296
+ components.street_suffix = type
297
+ }
298
+ } else if (type) {
299
+ countDropped(tally, "component:street_suffix:not-a-street-type")
300
+ }
301
+ }
302
+
303
+ const unit = present(record.UNIT)
304
+
305
+ if (unit && /^[A-Z0-9]{1,4}$/u.test(unit)) {
306
+ // `HOUSE_NUM` sometimes writes the unit letter after the number as well (`155 A` with unit `A`).
307
+ if (houseNumber.endsWith(` ${unit}`) || (/^[A-Z]$/u.test(unit) && houseNumber.endsWith(unit))) {
308
+ countDropped(tally, "component:unit:repeats-house-number")
309
+ } else {
310
+ components.unit = unit
311
+ }
312
+ } else if (unit) {
313
+ countDropped(tally, "component:unit:not-a-unit-designator")
314
+ }
315
+
316
+ const city = present(record.CITY)
317
+ const place = present(record.PLACENAME)
318
+
319
+ if (city && isGuamPlaceName(city)) {
320
+ components.locality = city
321
+ } else if (city) {
322
+ countDropped(tally, "component:locality:not-a-guam-place")
323
+ } else if (place && isGuamPlaceName(place)) {
324
+ components.locality = place
325
+ }
326
+
327
+ if (!components.locality) {
328
+ countDropped(tally, "row:no-locality")
329
+
330
+ return null
331
+ }
332
+
333
+ return components
334
+ }
335
+
336
+ /**
337
+ * The GeoJSON pages an input names: the file itself, or every `.geojson` in a directory, in name order.
338
+ */
339
+ async function pageFiles(inputPath: string): Promise<string[]> {
340
+ const stat = await tryStat(inputPath)
341
+
342
+ if (!stat?.isDirectory()) return [inputPath]
343
+
344
+ const names = await Globerator.from("*.geojson", { cwd: inputPath, absolute: false }).toSorted()
345
+
346
+ return names.map((name) => `${inputPath}/${name}`)
347
+ }
348
+
349
+ export function createGuamAddressPointsAdapter(): CorpusAdapter {
350
+ return {
351
+ id: BSP_GU_ADAPTER_ID,
352
+ defaultLicense: BSP_GU_LICENSE,
353
+ addressRole: AddressRole.Premise,
354
+ register: SourceRegister.GuamAddressPoints,
355
+ surface: SurfaceOrigin.Rendered,
356
+ description:
357
+ "Address_Point_(Public_View) (Government of Guam, Bureau of Statistics & Plans GIS): Guam's address points.",
358
+
359
+ async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
360
+ if (opts.country && !BSP_GU_COUNTRIES.includes(opts.country)) {
361
+ throw new UnsupportedCountryError(BSP_GU_ADAPTER_ID, BSP_GU_COUNTRIES, opts.country)
362
+ }
363
+
364
+ let emitted = 0
365
+
366
+ for (const file of await pageFiles(resolvePath(opts.inputPath))) {
367
+ // A query page holds at most the layer's 2,000-feature maxRecordCount, so each is parsed whole.
368
+ const collection = parseJSONStrict<GuamFeatureCollection>(await readLocalTextFile(file))
369
+
370
+ if (!Array.isArray(collection.features)) {
371
+ throw new TypeError(`${BSP_GU_ADAPTER_ID} adapter: ${file} holds no GeoJSON features array`)
372
+ }
373
+
374
+ for (const feature of collection.features) {
375
+ if (opts.signal?.aborted) return
376
+
377
+ if (opts.limit !== undefined && emitted >= opts.limit) return
378
+
379
+ const record = feature.properties
380
+
381
+ if (!record) {
382
+ countDropped(opts, "row:no-properties")
383
+
384
+ continue
385
+ }
386
+
387
+ const components = componentsForGuamRecord(record, opts)
388
+
389
+ if (!components) continue
390
+
391
+ const rendered = formatAddressRow(components, "GU", { singleLine: true })
392
+
393
+ if (!rendered) {
394
+ countDropped(opts, "row:render-failed")
395
+
396
+ continue
397
+ }
398
+
399
+ for (const tag of rendered.unplaced) {
400
+ countDropped(opts, `component:${tag}:not-rendered`)
401
+ }
402
+
403
+ const { raw, components: aligned } = rendered
404
+ const [lon, lat] = feature.geometry?.coordinates ?? []
405
+
406
+ yield {
407
+ raw,
408
+ components: aligned,
409
+ country: "GU",
410
+ locale: "en-GU",
411
+ source: BSP_GU_ADAPTER_ID,
412
+ // `FID` is the layer's object id, unique per feature.
413
+ // The point separates the units of one building that the rendered components may not.
414
+ source_id: stableSourceIDFromParts(BSP_GU_ADAPTER_ID, {
415
+ ...aligned,
416
+ fid: present(record.FID) ?? "",
417
+ lat: typeof lat === "number" ? String(lat) : "",
418
+ lon: typeof lon === "number" ? String(lon) : "",
419
+ }),
420
+ corpus_version: "",
421
+ license: BSP_GU_LICENSE,
422
+ }
423
+
424
+ emitted++
425
+ }
426
+ }
427
+ },
428
+ }
429
+ }
430
+
431
+ /**
432
+ * The configured adapter instance registered with the corpus builder.
433
+ */
434
+ export const guamAddressPointsAdapter = createGuamAddressPointsAdapter()