@mailwoman/corpus 7.2.0 → 7.2.1
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/package.json +59 -27
- package/src/adapter.ts +153 -0
- package/src/adapters/ban/adapter.ts +189 -0
- package/src/adapters/ban/street-decompose.ts +98 -0
- package/src/adapters/fcc-bdc/adapter.ts +188 -0
- package/src/adapters/geonames/adapter.ts +185 -0
- package/src/adapters/geonames-postal/adapter.ts +111 -0
- package/src/adapters/gnaf/adapter.ts +139 -0
- package/src/adapters/gnaf/assemble.ts +200 -0
- package/src/adapters/index.ts +135 -0
- package/src/adapters/openaddresses/adapter.ts +237 -0
- package/src/adapters/overture/adapter.ts +149 -0
- package/src/adapters/state-hi-schools/adapter.ts +171 -0
- package/src/adapters/state-ia-contractors/adapter.ts +148 -0
- package/src/adapters/state-ny-notaries/adapter.ts +169 -0
- package/src/adapters/state-tx-notaries/adapter.ts +160 -0
- package/src/adapters/synth-po-box/adapter.ts +197 -0
- package/src/adapters/tiger/adapter.ts +232 -0
- package/src/adapters/tiger/street-decompose.ts +120 -0
- package/src/adapters/usgov-hrsa-fqhc/adapter.ts +189 -0
- package/src/adapters/usgov-imls-pls/adapter.ts +152 -0
- package/src/adapters/usgov-irs-bmf/adapter.ts +155 -0
- package/src/adapters/usgov-nad/adapter.ts +338 -0
- package/src/adapters/usgov-nppes/adapter.ts +169 -0
- package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +211 -0
- package/src/adapters/wof-admin-jp/adapter.ts +176 -0
- package/src/adapters/wof-admin-json/adapter.ts +280 -0
- package/src/adapters/wof-postalcode-json/adapter.ts +209 -0
- package/src/align.ts +317 -0
- package/src/build.ts +348 -0
- package/src/codex/us-fips-state.ts +109 -0
- package/src/format.ts +14 -0
- package/src/golden.ts +147 -0
- package/src/index.ts +28 -0
- package/src/license.ts +48 -0
- package/src/parquet-wrapper/index.ts +9 -0
- package/src/parquet-wrapper/reader.ts +67 -0
- package/src/parquet-wrapper/schema.ts +80 -0
- package/src/parquet-wrapper/writer.ts +102 -0
- package/src/parquet.ts +360 -0
- package/src/runner.ts +283 -0
- package/src/shard-recipes/anchor-absorption.ts +86 -0
- package/src/shard-recipes/boundary-stress.ts +87 -0
- package/src/shard-recipes/country-balanced.ts +471 -0
- package/src/shard-recipes/cz-pcfirst-preposition.ts +83 -0
- package/src/shard-recipes/fr-admin-split.ts +212 -0
- package/src/shard-recipes/fr-bare-street.ts +85 -0
- package/src/shard-recipes/fr-fragment.ts +304 -0
- package/src/shard-recipes/fr-order.ts +283 -0
- package/src/shard-recipes/german.ts +206 -0
- package/src/shard-recipes/house-venue.ts +78 -0
- package/src/shard-recipes/index.ts +76 -0
- package/src/shard-recipes/intersection.ts +617 -0
- package/src/shard-recipes/locale.ts +415 -0
- package/src/shard-recipes/nl-postcode.ts +105 -0
- package/src/shard-recipes/no-fragment.ts +206 -0
- package/src/shard-recipes/no-street-led.ts +130 -0
- package/src/shard-recipes/no-street.ts +78 -0
- package/src/shard-recipes/po-box-cedex.ts +853 -0
- package/src/shard-recipes/po-box.ts +111 -0
- package/src/shard-recipes/scaffold.ts +187 -0
- package/src/shard-recipes/si-bare-village.ts +88 -0
- package/src/shard-recipes/street-affix.ts +549 -0
- package/src/shard-recipes/street-bare.ts +90 -0
- package/src/shard-recipes/street.ts +74 -0
- package/src/shard-recipes/unit.ts +315 -0
- package/src/split.ts +251 -0
- package/src/synthesize-anchor-absorption.ts +263 -0
- package/src/synthesize-boundary-stress.ts +504 -0
- package/src/synthesize-german.ts +227 -0
- package/src/synthesize-house-venue.ts +192 -0
- package/src/synthesize-intersection.ts +205 -0
- package/src/synthesize-no-street.ts +341 -0
- package/src/synthesize-po-box.ts +307 -0
- package/src/synthesize-street.ts +264 -0
- package/src/synthesize.ts +872 -0
- package/src/tokenize.ts +68 -0
- package/src/tools/align-shard.ts +67 -0
- package/src/tools/audit.ts +352 -0
- package/src/tools/corpus-stats.ts +183 -0
- package/src/tools/fetch/ban.ts +288 -0
- package/src/tools/fetch/download.ts +119 -0
- package/src/tools/fetch/hrsa.ts +65 -0
- package/src/tools/fetch/imls-pls.ts +162 -0
- package/src/tools/fetch/index.ts +121 -0
- package/src/tools/fetch/nad.ts +304 -0
- package/src/tools/fetch/nppes.ts +177 -0
- package/src/tools/fetch/openaddresses.ts +380 -0
- package/src/tools/fetch/state-hi-schools.ts +217 -0
- package/src/tools/fetch/state-sources.ts +142 -0
- package/src/tools/fetch/tiger-full.ts +318 -0
- package/src/tools/golden-expand.ts +524 -0
- package/src/tools/golden-promote.ts +304 -0
- package/src/tools/index.ts +24 -0
- package/src/tools/ingest-csv.ts +420 -0
- package/src/tools/jsonl-to-parquet.ts +200 -0
- package/src/tools/lint-shard-vocab.ts +357 -0
- package/src/tools/lint-shard.ts +502 -0
- package/src/tools/overlay-manifest.ts +141 -0
- package/src/tools/shard-kryptonite.ts +131 -0
- package/src/tools/shard-translit.ts +244 -0
- package/src/types.ts +213 -0
- package/src/wof-json.ts +236 -0
package/src/wof-json.ts
ADDED
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* Shared utilities for the `wof-admin` / `wof-postalcode` GeoJSON-bundle adapters.
|
|
7
|
+
*
|
|
8
|
+
* The Phase 1.5.1 pivot moved both adapters off the SpatiaLite distribution (dead mirror, empty
|
|
9
|
+
* `names` table) and onto the per-record GeoJSON bundles published as
|
|
10
|
+
* `github.com/whosonfirst-data/whosonfirst-data-{admin,postalcode}-<cc>` repos. Each repo carries
|
|
11
|
+
* a tree of `data/<3>/<3>/<3>/<wof-id>.geojson` files plus alternate-geometry siblings like
|
|
12
|
+
* `<id>-alt-quattroshapes.geojson`. The adapter only consumes the canonical record (no `-alt-`
|
|
13
|
+
* files); the alternate geometries are irrelevant to the name/hierarchy concerns Phase 1 cares
|
|
14
|
+
* about.
|
|
15
|
+
*
|
|
16
|
+
* This module provides:
|
|
17
|
+
*
|
|
18
|
+
* - `WOFRecord`: the lightweight per-feature shape both adapters carry in their ancestry index.
|
|
19
|
+
* - `walkFeatures`: streaming directory walk → parsed `WOFRecord`s (skips alt files, bad JSON,
|
|
20
|
+
* deprecated records).
|
|
21
|
+
* - `buildAncestryIndex`: in-memory ancestry chain construction (`Map<id, ancestors[]>`).
|
|
22
|
+
* - `extractNameVariants`: pulls `name:*` localized name lists off a feature's properties.
|
|
23
|
+
* - `normalizeNameKey`: turns `"name:eng_x_colloquial"` into `"name-eng-x-colloquial"` for safe use
|
|
24
|
+
* in `source_id` suffixes.
|
|
25
|
+
*
|
|
26
|
+
* `is_current` semantics follow WOF + Pelias: `mz:is_current` ∈ {`1`, `-1`} are live; `0` is
|
|
27
|
+
* superseded. WOF's official postalcode distribution stamps every row with `-1` ("unknown but
|
|
28
|
+
* treated as active"), which is why the previous SpatiaLite adapter's `is_current = 1` filter
|
|
29
|
+
* silently emitted zero rows from the real corpus.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readFile } from "node:fs/promises"
|
|
33
|
+
|
|
34
|
+
import FastGlob from "fast-glob"
|
|
35
|
+
|
|
36
|
+
/** A WOF GeoJSON feature, as published by the per-record bundles. */
|
|
37
|
+
export interface WOFFeature {
|
|
38
|
+
type?: string
|
|
39
|
+
id?: number | string
|
|
40
|
+
properties?: Record<string, unknown> | null
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Lightweight in-memory shape both adapters keep per record. Geometry is intentionally dropped — it's 95% of the file
|
|
45
|
+
* weight and the adapters never consult it.
|
|
46
|
+
*/
|
|
47
|
+
export interface WOFRecord {
|
|
48
|
+
id: number
|
|
49
|
+
parent_id: number | null
|
|
50
|
+
/** Canonical `wof:name` of the record. */
|
|
51
|
+
name: string
|
|
52
|
+
placetype: string
|
|
53
|
+
/** ISO 3166-1 alpha-2 from `wof:country`. */
|
|
54
|
+
country: string
|
|
55
|
+
/**
|
|
56
|
+
* Localized name variants from `name:*` properties.
|
|
57
|
+
*
|
|
58
|
+
* Keys are the raw `name:eng_x_preferred` form; values are the first non-empty string from the underlying array (WOF
|
|
59
|
+
* stores variants as arrays even when only one form is present). The canonical `wof:name` is NOT included here —
|
|
60
|
+
* adapters add a synthetic `"default"` slot for it.
|
|
61
|
+
*/
|
|
62
|
+
nameVariants: Map<string, string>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* `mz:is_current` ∈ {`1`, `-1`} → keep. `0` → drop.
|
|
67
|
+
*
|
|
68
|
+
* Real WOF postalcode distros tag every row `-1` ("unknown but treated as active"); the Pelias importer accepts `-1`
|
|
69
|
+
* alongside `1`. The previous SpatiaLite-backed adapters filtered on `= 1` only and silently emitted zero rows from the
|
|
70
|
+
* corpus — this loosened predicate is the critical fix.
|
|
71
|
+
*/
|
|
72
|
+
export function isCurrentFeature(props: Record<string, unknown>): boolean {
|
|
73
|
+
const raw = props["mz:is_current"]
|
|
74
|
+
const n = typeof raw === "number" ? raw : typeof raw === "string" ? Number(raw) : 1
|
|
75
|
+
|
|
76
|
+
return n !== 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Pull `name:*` localized variants off a WOF feature's properties. WOF stores variants as arrays (`["Saint
|
|
81
|
+
* Petersburg"]`); we lift the first non-empty string. Multiple-value variants (rare; usually historical aliases) are
|
|
82
|
+
* not split into separate rows by this helper — adapters can opt in by iterating the underlying array if they need it.
|
|
83
|
+
*/
|
|
84
|
+
export function extractNameVariants(props: Record<string, unknown>): Map<string, string> {
|
|
85
|
+
const out = new Map<string, string>()
|
|
86
|
+
|
|
87
|
+
for (const [key, value] of Object.entries(props)) {
|
|
88
|
+
if (!key.startsWith("name:")) continue
|
|
89
|
+
const candidate = Array.isArray(value)
|
|
90
|
+
? value.find((v): v is string => typeof v === "string" && v.trim().length > 0)
|
|
91
|
+
: typeof value === "string" && value.trim().length > 0
|
|
92
|
+
? value
|
|
93
|
+
: undefined
|
|
94
|
+
|
|
95
|
+
if (candidate) {
|
|
96
|
+
out.set(key, candidate.trim())
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Turn a `name:*` property key into a hyphen-safe suffix fragment for `source_id`.
|
|
105
|
+
*
|
|
106
|
+
* `"name:eng_x_colloquial"` → `"name-eng-x-colloquial"`. `:` and `_` both become `-` because both collide with the
|
|
107
|
+
* existing source_id separator vocabulary and downstream consumers split on `-`.
|
|
108
|
+
*/
|
|
109
|
+
export function normalizeNameKey(rawKey: string): string {
|
|
110
|
+
return rawKey.replace(/[:_]/g, "-")
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Result of parsing a single GeoJSON file. `null` means "skip this row" (any reason). */
|
|
114
|
+
function recordFromFeature(feature: WOFFeature): WOFRecord | null {
|
|
115
|
+
if (!feature || feature.type !== "Feature" || !feature.properties) return null
|
|
116
|
+
const props = feature.properties
|
|
117
|
+
|
|
118
|
+
const rawID = typeof feature.id === "number" ? feature.id : props["wof:id"]
|
|
119
|
+
const id = typeof rawID === "number" ? rawID : typeof rawID === "string" ? Number(rawID) : NaN
|
|
120
|
+
|
|
121
|
+
if (!Number.isFinite(id)) return null
|
|
122
|
+
|
|
123
|
+
const name = props["wof:name"]
|
|
124
|
+
|
|
125
|
+
if (typeof name !== "string" || !name.trim()) return null
|
|
126
|
+
|
|
127
|
+
const placetype = props["wof:placetype"]
|
|
128
|
+
|
|
129
|
+
if (typeof placetype !== "string" || !placetype) return null
|
|
130
|
+
|
|
131
|
+
const country = props["wof:country"]
|
|
132
|
+
|
|
133
|
+
if (typeof country !== "string" || !country) return null
|
|
134
|
+
|
|
135
|
+
if (!isCurrentFeature(props)) return null
|
|
136
|
+
|
|
137
|
+
const parentRaw = props["wof:parent_id"]
|
|
138
|
+
const parent_id =
|
|
139
|
+
typeof parentRaw === "number"
|
|
140
|
+
? parentRaw
|
|
141
|
+
: typeof parentRaw === "string" && parentRaw.trim()
|
|
142
|
+
? Number(parentRaw)
|
|
143
|
+
: null
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
id,
|
|
147
|
+
parent_id: Number.isFinite(parent_id as number) ? (parent_id as number) : null,
|
|
148
|
+
name: name.trim(),
|
|
149
|
+
placetype,
|
|
150
|
+
country,
|
|
151
|
+
nameVariants: extractNameVariants(props),
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Stream every canonical GeoJSON file under `repoDir` and yield parsed `WOFRecord`s.
|
|
157
|
+
*
|
|
158
|
+
* `repoDir` may point at a single cloned `whosonfirst-data-*` repo OR at a parent directory holding several such repos
|
|
159
|
+
* (the corpus pipeline clones all four into a shared `wof/repos/` root and runs the adapter against that root).
|
|
160
|
+
* `**\/*.geojson` walks the whole tree; `-alt-` siblings are skipped since they're alternate-geometry exports, not new
|
|
161
|
+
* records.
|
|
162
|
+
*
|
|
163
|
+
* Errors per-file (unreadable, malformed JSON, missing properties) are swallowed so one bad file doesn't poison a 3 GB
|
|
164
|
+
* walk. Adapters can add stricter validation downstream if they need it.
|
|
165
|
+
*/
|
|
166
|
+
export async function* walkFeatures(repoDir: string, opts: { signal?: AbortSignal } = {}): AsyncIterable<WOFRecord> {
|
|
167
|
+
const stream = FastGlob.stream(["**/*.geojson"], {
|
|
168
|
+
cwd: repoDir,
|
|
169
|
+
absolute: true,
|
|
170
|
+
onlyFiles: true,
|
|
171
|
+
suppressErrors: true,
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
for await (const entry of stream) {
|
|
175
|
+
if (opts.signal?.aborted) return
|
|
176
|
+
const filePath = String(entry)
|
|
177
|
+
|
|
178
|
+
if (filePath.includes("-alt-")) continue
|
|
179
|
+
|
|
180
|
+
let text: string
|
|
181
|
+
|
|
182
|
+
try {
|
|
183
|
+
text = await readFile(filePath, "utf8")
|
|
184
|
+
} catch {
|
|
185
|
+
continue
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
let parsed: WOFFeature
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
parsed = JSON.parse(text) as WOFFeature
|
|
192
|
+
} catch {
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const rec = recordFromFeature(parsed)
|
|
197
|
+
|
|
198
|
+
if (rec) {
|
|
199
|
+
yield rec
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Build an in-memory ancestry index: `Map<wof_id, [parent, grandparent, ...]>` walking `parent_id` upward and stopping
|
|
206
|
+
* at the first missing link. A cycle guard halts at any re-visit (defensive — WOF data is acyclic by construction but
|
|
207
|
+
* corrupt fixtures shouldn't infinite-loop the adapter).
|
|
208
|
+
*
|
|
209
|
+
* Records whose ancestors aren't in `byID` (e.g. an FR locality whose region wasn't included in the cloned repo set)
|
|
210
|
+
* get a shorter chain; the variant emission gracefully degrades.
|
|
211
|
+
*/
|
|
212
|
+
export type AncestryIndex = Map<number, WOFRecord[]>
|
|
213
|
+
|
|
214
|
+
export function buildAncestryIndex(byID: Map<number, WOFRecord>): AncestryIndex {
|
|
215
|
+
const index: AncestryIndex = new Map()
|
|
216
|
+
|
|
217
|
+
for (const [id, rec] of byID) {
|
|
218
|
+
const chain: WOFRecord[] = []
|
|
219
|
+
const guard = new Set<number>([id])
|
|
220
|
+
let cur: number | null = rec.parent_id
|
|
221
|
+
|
|
222
|
+
while (cur !== null && cur > 0) {
|
|
223
|
+
const parent = byID.get(cur)
|
|
224
|
+
|
|
225
|
+
if (!parent) break
|
|
226
|
+
|
|
227
|
+
if (guard.has(parent.id)) break
|
|
228
|
+
chain.push(parent)
|
|
229
|
+
guard.add(parent.id)
|
|
230
|
+
cur = parent.parent_id
|
|
231
|
+
}
|
|
232
|
+
index.set(id, chain)
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return index
|
|
236
|
+
}
|