@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.
- package/data/PROVENANCE.md +24 -0
- package/data/address-source-register.json +18948 -8757
- package/data/corpus-source-inputs.json +4756 -0
- package/data/license-decisions.json +7260 -0
- package/data/licenses/whosonfirst-licenses.md +3 -2
- package/data/reviewed-ve-postcode-tuples.json +5 -72
- package/data/source-additions.json +2450 -0
- package/data/source-resolutions.json +87 -0
- package/data/training-manifests/v0.7.10-gb-county.effective.json +2335 -0
- package/data/training-manifests/v0.7.10-gb-county.json +1132 -0
- package/data/training-manifests/v0.7.2-address-systems.effective.json +3 -2
- package/data/training-manifests/v0.7.7-no-ppd.json +550 -0
- package/data/training-manifests/v0.7.8-rights-clean.json +538 -0
- package/data/training-manifests/v0.7.9-country-gap.json +1132 -0
- package/data/us-situs-sources.json +128 -0
- package/lib/adapters/cordis/adapter.ts +1 -1
- package/lib/adapters/libpostal-dictionary.ts +23 -3
- package/lib/adapters/osm/adapter.ts +42 -11
- package/lib/adapters.ts +14 -0
- package/lib/az/adapters/opendata/adapter.ts +389 -0
- package/lib/bs/adapters/gbpa/adapter.ts +248 -0
- package/lib/build/types.ts +16 -0
- package/lib/build.ts +21 -0
- package/lib/ee/adapters/ads/adapter.ts +18 -0
- package/lib/fo/adapters/adressur/adapter.ts +242 -0
- package/lib/gb/adapters/os-open/adapter.ts +14 -2
- package/lib/gb/county.ts +59 -0
- package/lib/gb/os-open/{open-names.ts → open/names.ts} +41 -0
- package/lib/gu/adapters/bsp/adapter.ts +434 -0
- package/lib/im/adapters/land-registry/adapter.ts +340 -0
- package/lib/international/recipes/locale.ts +164 -19
- package/lib/li/adapters/llv/adapter.ts +246 -0
- package/lib/recipes/bare/postcode.ts +10 -34
- package/lib/recipes/country-balanced.ts +95 -78
- package/lib/recipes/intersection.ts +22 -30
- package/lib/recipes/po/box/cedex/recipe.ts +8 -5
- package/lib/recipes/po/box/cedex/sources.ts +2 -23
- package/lib/recipes/scaffold.ts +28 -3
- package/lib/recipes/sources.ts +112 -1
- package/lib/recipes/street/affix.ts +17 -29
- package/lib/recipes/sub/venue/context.ts +33 -9
- package/lib/recipes/sub/venue/sources.ts +54 -13
- package/lib/recipes/sub/venue.ts +68 -23
- package/lib/recipes/unit.ts +20 -7
- package/lib/registers.ts +33 -0
- package/lib/source-register/corpus-inputs.ts +528 -0
- package/lib/source-register/effective-manifest.ts +37 -5
- package/lib/source-register/types.ts +20 -0
- package/lib/south-asia/recipes/register.ts +1 -1
- package/lib/tools/overlay/manifest.ts +109 -1
- package/lib/tools/source-register/build.ts +47 -1
- package/lib/tools/variety-sample.ts +633 -0
- package/lib/us/adapters/usgov/nad/adapter.ts +39 -10
- package/lib/us/recipes/skeleton-sources.ts +156 -0
- package/lib/us/tools/nad-skeletons.ts +395 -0
- package/lib/uy/adapters/ide/adapter.ts +275 -0
- package/lib/ve/recipes/reviewed-postcode-tail.ts +29 -6
- package/out/adapters/cordis/adapter.d.ts +1 -1
- package/out/adapters/cordis/adapter.js +1 -1
- package/out/adapters/libpostal-dictionary.d.ts +9 -0
- package/out/adapters/libpostal-dictionary.d.ts.map +1 -1
- package/out/adapters/libpostal-dictionary.js +19 -3
- package/out/adapters/libpostal-dictionary.js.map +1 -1
- package/out/adapters/osm/adapter.d.ts +12 -1
- package/out/adapters/osm/adapter.d.ts.map +1 -1
- package/out/adapters/osm/adapter.js +30 -13
- package/out/adapters/osm/adapter.js.map +1 -1
- package/out/adapters.d.ts.map +1 -1
- package/out/adapters.js +14 -0
- package/out/adapters.js.map +1 -1
- package/out/az/adapters/opendata/adapter.d.ts +88 -0
- package/out/az/adapters/opendata/adapter.d.ts.map +1 -0
- package/out/az/adapters/opendata/adapter.js +282 -0
- package/out/az/adapters/opendata/adapter.js.map +1 -0
- package/out/bs/adapters/gbpa/adapter.d.ts +64 -0
- package/out/bs/adapters/gbpa/adapter.d.ts.map +1 -0
- package/out/bs/adapters/gbpa/adapter.js +187 -0
- package/out/bs/adapters/gbpa/adapter.js.map +1 -0
- package/out/build/types.d.ts +15 -0
- package/out/build/types.d.ts.map +1 -1
- package/out/build/types.js.map +1 -1
- package/out/build.d.ts.map +1 -1
- package/out/build.js +17 -0
- package/out/build.js.map +1 -1
- package/out/ee/adapters/ads/adapter.d.ts +6 -0
- package/out/ee/adapters/ads/adapter.d.ts.map +1 -1
- package/out/ee/adapters/ads/adapter.js +16 -0
- package/out/ee/adapters/ads/adapter.js.map +1 -1
- package/out/fo/adapters/adressur/adapter.d.ts +63 -0
- package/out/fo/adapters/adressur/adapter.d.ts.map +1 -0
- package/out/fo/adapters/adressur/adapter.js +177 -0
- package/out/fo/adapters/adressur/adapter.js.map +1 -0
- package/out/gb/adapters/os-open/adapter.d.ts +10 -2
- package/out/gb/adapters/os-open/adapter.d.ts.map +1 -1
- package/out/gb/adapters/os-open/adapter.js +13 -2
- package/out/gb/adapters/os-open/adapter.js.map +1 -1
- package/out/gb/county.d.ts +30 -0
- package/out/gb/county.d.ts.map +1 -0
- package/out/gb/county.js +51 -0
- package/out/gb/county.js.map +1 -0
- package/out/gb/os-open/{open-names.d.ts → open/names.d.ts} +18 -1
- package/out/gb/os-open/open/names.d.ts.map +1 -0
- package/out/gb/os-open/{open-names.js → open/names.js} +20 -1
- package/out/gb/os-open/open/names.js.map +1 -0
- package/out/gb/os-open/{open-roads.d.ts → open/roads.d.ts} +1 -1
- package/out/gb/os-open/open/roads.d.ts.map +1 -0
- package/out/gb/os-open/{open-roads.js → open/roads.js} +1 -1
- package/out/gb/os-open/open/roads.js.map +1 -0
- package/out/gu/adapters/bsp/adapter.d.ts +83 -0
- package/out/gu/adapters/bsp/adapter.d.ts.map +1 -0
- package/out/gu/adapters/bsp/adapter.js +346 -0
- package/out/gu/adapters/bsp/adapter.js.map +1 -0
- package/out/im/adapters/land-registry/adapter.d.ts +68 -0
- package/out/im/adapters/land-registry/adapter.d.ts.map +1 -0
- package/out/im/adapters/land-registry/adapter.js +261 -0
- package/out/im/adapters/land-registry/adapter.js.map +1 -0
- package/out/international/recipes/locale.d.ts +24 -4
- package/out/international/recipes/locale.d.ts.map +1 -1
- package/out/international/recipes/locale.js +107 -15
- package/out/international/recipes/locale.js.map +1 -1
- package/out/li/adapters/llv/adapter.d.ts +69 -0
- package/out/li/adapters/llv/adapter.d.ts.map +1 -0
- package/out/li/adapters/llv/adapter.js +181 -0
- package/out/li/adapters/llv/adapter.js.map +1 -0
- package/out/recipes/bare/postcode.d.ts.map +1 -1
- package/out/recipes/bare/postcode.js +9 -30
- package/out/recipes/bare/postcode.js.map +1 -1
- package/out/recipes/country-balanced.d.ts.map +1 -1
- package/out/recipes/country-balanced.js +82 -73
- package/out/recipes/country-balanced.js.map +1 -1
- package/out/recipes/intersection.d.ts +3 -2
- package/out/recipes/intersection.d.ts.map +1 -1
- package/out/recipes/intersection.js +20 -26
- package/out/recipes/intersection.js.map +1 -1
- package/out/recipes/po/box/cedex/recipe.d.ts.map +1 -1
- package/out/recipes/po/box/cedex/recipe.js +5 -4
- package/out/recipes/po/box/cedex/recipe.js.map +1 -1
- package/out/recipes/po/box/cedex/sources.d.ts +2 -45
- package/out/recipes/po/box/cedex/sources.d.ts.map +1 -1
- package/out/recipes/po/box/cedex/sources.js +0 -20
- package/out/recipes/po/box/cedex/sources.js.map +1 -1
- package/out/recipes/scaffold.d.ts +15 -2
- package/out/recipes/scaffold.d.ts.map +1 -1
- package/out/recipes/scaffold.js +21 -1
- package/out/recipes/scaffold.js.map +1 -1
- package/out/recipes/sources.d.ts.map +1 -1
- package/out/recipes/sources.js +112 -1
- package/out/recipes/sources.js.map +1 -1
- package/out/recipes/street/affix.d.ts.map +1 -1
- package/out/recipes/street/affix.js +8 -20
- package/out/recipes/street/affix.js.map +1 -1
- package/out/recipes/sub/venue/context.d.ts +14 -3
- package/out/recipes/sub/venue/context.d.ts.map +1 -1
- package/out/recipes/sub/venue/context.js +29 -9
- package/out/recipes/sub/venue/context.js.map +1 -1
- package/out/recipes/sub/venue/sources.d.ts +41 -8
- package/out/recipes/sub/venue/sources.d.ts.map +1 -1
- package/out/recipes/sub/venue/sources.js +26 -7
- package/out/recipes/sub/venue/sources.js.map +1 -1
- package/out/recipes/sub/venue.d.ts.map +1 -1
- package/out/recipes/sub/venue.js +35 -14
- package/out/recipes/sub/venue.js.map +1 -1
- package/out/recipes/unit.d.ts +4 -0
- package/out/recipes/unit.d.ts.map +1 -1
- package/out/recipes/unit.js +7 -6
- package/out/recipes/unit.js.map +1 -1
- package/out/registers.d.ts +33 -0
- package/out/registers.d.ts.map +1 -1
- package/out/registers.js +33 -0
- package/out/registers.js.map +1 -1
- package/out/source-register/corpus-inputs.d.ts +240 -0
- package/out/source-register/corpus-inputs.d.ts.map +1 -0
- package/out/source-register/corpus-inputs.js +275 -0
- package/out/source-register/corpus-inputs.js.map +1 -0
- package/out/source-register/effective-manifest.d.ts +2 -0
- package/out/source-register/effective-manifest.d.ts.map +1 -1
- package/out/source-register/effective-manifest.js +33 -4
- package/out/source-register/effective-manifest.js.map +1 -1
- package/out/source-register/types.d.ts +21 -1
- package/out/source-register/types.d.ts.map +1 -1
- package/out/source-register/types.js +20 -0
- package/out/source-register/types.js.map +1 -1
- package/out/south-asia/recipes/register.js +1 -1
- package/out/south-asia/recipes/register.js.map +1 -1
- package/out/tools/overlay/manifest.d.ts +7 -0
- package/out/tools/overlay/manifest.d.ts.map +1 -1
- package/out/tools/overlay/manifest.js +72 -1
- package/out/tools/overlay/manifest.js.map +1 -1
- package/out/tools/source-register/build.d.ts +9 -0
- package/out/tools/source-register/build.d.ts.map +1 -1
- package/out/tools/source-register/build.js +25 -3
- package/out/tools/source-register/build.js.map +1 -1
- package/out/tools/variety-sample.d.ts +196 -0
- package/out/tools/variety-sample.d.ts.map +1 -0
- package/out/tools/variety-sample.js +417 -0
- package/out/tools/variety-sample.js.map +1 -0
- package/out/us/adapters/usgov/nad/adapter.d.ts +80 -0
- package/out/us/adapters/usgov/nad/adapter.d.ts.map +1 -1
- package/out/us/adapters/usgov/nad/adapter.js +27 -8
- package/out/us/adapters/usgov/nad/adapter.js.map +1 -1
- package/out/us/recipes/skeleton-sources.d.ts +92 -0
- package/out/us/recipes/skeleton-sources.d.ts.map +1 -0
- package/out/us/recipes/skeleton-sources.js +121 -0
- package/out/us/recipes/skeleton-sources.js.map +1 -0
- package/out/us/tools/nad-skeletons.d.ts +120 -0
- package/out/us/tools/nad-skeletons.d.ts.map +1 -0
- package/out/us/tools/nad-skeletons.js +262 -0
- package/out/us/tools/nad-skeletons.js.map +1 -0
- package/out/uy/adapters/ide/adapter.d.ts +64 -0
- package/out/uy/adapters/ide/adapter.d.ts.map +1 -0
- package/out/uy/adapters/ide/adapter.js +206 -0
- package/out/uy/adapters/ide/adapter.js.map +1 -0
- package/out/ve/recipes/reviewed-postcode-tail.d.ts +6 -2
- package/out/ve/recipes/reviewed-postcode-tail.d.ts.map +1 -1
- package/out/ve/recipes/reviewed-postcode-tail.js +16 -5
- package/out/ve/recipes/reviewed-postcode-tail.js.map +1 -1
- package/package.json +14 -14
- package/lib/recipes/unit/sources.ts +0 -44
- package/lib/tools/fetch/ppd.ts +0 -121
- package/out/gb/os-open/open-names.d.ts.map +0 -1
- package/out/gb/os-open/open-names.js.map +0 -1
- package/out/gb/os-open/open-roads.d.ts.map +0 -1
- package/out/gb/os-open/open-roads.js.map +0 -1
- package/out/recipes/unit/sources.d.ts +0 -28
- package/out/recipes/unit/sources.d.ts.map +0 -1
- package/out/recipes/unit/sources.js +0 -30
- package/out/recipes/unit/sources.js.map +0 -1
- package/out/tools/fetch/ppd.d.ts +0 -15
- package/out/tools/fetch/ppd.d.ts.map +0 -1
- package/out/tools/fetch/ppd.js +0 -86
- package/out/tools/fetch/ppd.js.map +0 -1
- /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()
|