@mailwoman/corpus 7.2.0 → 7.3.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 (103) hide show
  1. package/package.json +59 -27
  2. package/src/adapter.ts +153 -0
  3. package/src/adapters/ban/adapter.ts +189 -0
  4. package/src/adapters/ban/street-decompose.ts +98 -0
  5. package/src/adapters/fcc-bdc/adapter.ts +188 -0
  6. package/src/adapters/geonames/adapter.ts +185 -0
  7. package/src/adapters/geonames-postal/adapter.ts +111 -0
  8. package/src/adapters/gnaf/adapter.ts +139 -0
  9. package/src/adapters/gnaf/assemble.ts +200 -0
  10. package/src/adapters/index.ts +135 -0
  11. package/src/adapters/openaddresses/adapter.ts +237 -0
  12. package/src/adapters/overture/adapter.ts +149 -0
  13. package/src/adapters/state-hi-schools/adapter.ts +171 -0
  14. package/src/adapters/state-ia-contractors/adapter.ts +148 -0
  15. package/src/adapters/state-ny-notaries/adapter.ts +169 -0
  16. package/src/adapters/state-tx-notaries/adapter.ts +160 -0
  17. package/src/adapters/synth-po-box/adapter.ts +197 -0
  18. package/src/adapters/tiger/adapter.ts +232 -0
  19. package/src/adapters/tiger/street-decompose.ts +120 -0
  20. package/src/adapters/usgov-hrsa-fqhc/adapter.ts +189 -0
  21. package/src/adapters/usgov-imls-pls/adapter.ts +152 -0
  22. package/src/adapters/usgov-irs-bmf/adapter.ts +155 -0
  23. package/src/adapters/usgov-nad/adapter.ts +338 -0
  24. package/src/adapters/usgov-nppes/adapter.ts +169 -0
  25. package/src/adapters/usgov-samhsa-treatment-locator/adapter.ts +211 -0
  26. package/src/adapters/wof-admin-jp/adapter.ts +176 -0
  27. package/src/adapters/wof-admin-json/adapter.ts +280 -0
  28. package/src/adapters/wof-postalcode-json/adapter.ts +209 -0
  29. package/src/align.ts +317 -0
  30. package/src/build.ts +348 -0
  31. package/src/codex/us-fips-state.ts +109 -0
  32. package/src/format.ts +14 -0
  33. package/src/golden.ts +147 -0
  34. package/src/index.ts +28 -0
  35. package/src/license.ts +48 -0
  36. package/src/parquet-wrapper/index.ts +9 -0
  37. package/src/parquet-wrapper/reader.ts +67 -0
  38. package/src/parquet-wrapper/schema.ts +80 -0
  39. package/src/parquet-wrapper/writer.ts +102 -0
  40. package/src/parquet.ts +360 -0
  41. package/src/runner.ts +283 -0
  42. package/src/shard-recipes/anchor-absorption.ts +86 -0
  43. package/src/shard-recipes/boundary-stress.ts +87 -0
  44. package/src/shard-recipes/country-balanced.ts +471 -0
  45. package/src/shard-recipes/cz-pcfirst-preposition.ts +83 -0
  46. package/src/shard-recipes/fr-admin-split.ts +212 -0
  47. package/src/shard-recipes/fr-bare-street.ts +85 -0
  48. package/src/shard-recipes/fr-fragment.ts +304 -0
  49. package/src/shard-recipes/fr-order.ts +283 -0
  50. package/src/shard-recipes/german.ts +206 -0
  51. package/src/shard-recipes/house-venue.ts +78 -0
  52. package/src/shard-recipes/index.ts +76 -0
  53. package/src/shard-recipes/intersection.ts +617 -0
  54. package/src/shard-recipes/locale.ts +415 -0
  55. package/src/shard-recipes/nl-postcode.ts +105 -0
  56. package/src/shard-recipes/no-fragment.ts +206 -0
  57. package/src/shard-recipes/no-street-led.ts +130 -0
  58. package/src/shard-recipes/no-street.ts +78 -0
  59. package/src/shard-recipes/po-box-cedex.ts +853 -0
  60. package/src/shard-recipes/po-box.ts +111 -0
  61. package/src/shard-recipes/scaffold.ts +187 -0
  62. package/src/shard-recipes/si-bare-village.ts +88 -0
  63. package/src/shard-recipes/street-affix.ts +549 -0
  64. package/src/shard-recipes/street-bare.ts +90 -0
  65. package/src/shard-recipes/street.ts +74 -0
  66. package/src/shard-recipes/unit.ts +315 -0
  67. package/src/split.ts +251 -0
  68. package/src/synthesize-anchor-absorption.ts +263 -0
  69. package/src/synthesize-boundary-stress.ts +504 -0
  70. package/src/synthesize-german.ts +227 -0
  71. package/src/synthesize-house-venue.ts +192 -0
  72. package/src/synthesize-intersection.ts +205 -0
  73. package/src/synthesize-no-street.ts +341 -0
  74. package/src/synthesize-po-box.ts +307 -0
  75. package/src/synthesize-street.ts +264 -0
  76. package/src/synthesize.ts +872 -0
  77. package/src/tokenize.ts +68 -0
  78. package/src/tools/align-shard.ts +67 -0
  79. package/src/tools/audit.ts +352 -0
  80. package/src/tools/corpus-stats.ts +183 -0
  81. package/src/tools/fetch/ban.ts +288 -0
  82. package/src/tools/fetch/download.ts +119 -0
  83. package/src/tools/fetch/hrsa.ts +65 -0
  84. package/src/tools/fetch/imls-pls.ts +162 -0
  85. package/src/tools/fetch/index.ts +121 -0
  86. package/src/tools/fetch/nad.ts +304 -0
  87. package/src/tools/fetch/nppes.ts +177 -0
  88. package/src/tools/fetch/openaddresses.ts +380 -0
  89. package/src/tools/fetch/state-hi-schools.ts +217 -0
  90. package/src/tools/fetch/state-sources.ts +142 -0
  91. package/src/tools/fetch/tiger-full.ts +318 -0
  92. package/src/tools/golden-expand.ts +524 -0
  93. package/src/tools/golden-promote.ts +304 -0
  94. package/src/tools/index.ts +24 -0
  95. package/src/tools/ingest-csv.ts +420 -0
  96. package/src/tools/jsonl-to-parquet.ts +200 -0
  97. package/src/tools/lint-shard-vocab.ts +357 -0
  98. package/src/tools/lint-shard.ts +502 -0
  99. package/src/tools/overlay-manifest.ts +141 -0
  100. package/src/tools/shard-kryptonite.ts +131 -0
  101. package/src/tools/shard-translit.ts +244 -0
  102. package/src/types.ts +213 -0
  103. package/src/wof-json.ts +236 -0
@@ -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
+ }