@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.
- 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
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `overture`: Overture Maps Addresses adapter (epic #470 — the gated corpus adapter, realized
|
|
7
|
+
* 2026-06-20). Overture's global Addresses theme is the single-schema, well-normalized address
|
|
8
|
+
* dataset that fixes OpenAddresses' per-country patchiness (OA dropped Spain; OA-DE omits the
|
|
9
|
+
* Bundesland) — it even re-hosts the OA Spain data the standalone OA bucket no longer serves.
|
|
10
|
+
*
|
|
11
|
+
* This adapter consumes a per-country LINE-DELIMITED JSON dump of the corpus-relevant fields (`{
|
|
12
|
+
* street, number, unit, postcode, locality }`), produced by `scripts/ingest-overture-addresses.ts
|
|
13
|
+
* --corpus-jsonl` (which does the DuckDB / S3 heavy lifting and flattens `address_levels` → the
|
|
14
|
+
* municipality locality). The split keeps `@mailwoman/corpus` — a RUNTIME dep of the `mailwoman`
|
|
15
|
+
* CLI — free of the heavy native `@duckdb/node-api`; the adapter just streams JSONL line-by-line,
|
|
16
|
+
* exactly like `openaddresses`.
|
|
17
|
+
*
|
|
18
|
+
* The `street` surface carries the locale's street keyword verbatim (`"CALLE JULAN"`, `"VIA
|
|
19
|
+
* ROMA"`). We map it to `street` whole and let the downstream affix-relabel split `street_prefix`
|
|
20
|
+
* — the same path every other source rides. This shard exists because the model was
|
|
21
|
+
* en-us/fr-trained and never saw non-en/fr street formats (the 2026-06-19 EU parse-blocker
|
|
22
|
+
* measured loc-correct ES 21% / IT 59% / NL 64% vs FR/US ~98%).
|
|
23
|
+
*
|
|
24
|
+
* `--country` is REQUIRED (the JSONL is per-country and the rows omit a country field), matching
|
|
25
|
+
* `openaddresses`. License is Overture's CDLA-Permissive-2.0 (attribution; not share-alike).
|
|
26
|
+
*
|
|
27
|
+
* | Field | ComponentTag | | --------- | ---------------------------------------------- | |
|
|
28
|
+
* `street` | `street` (keyword incl.; affix-relabel splits prefix) | | `number` | `house_number`
|
|
29
|
+
* (skipped when "S-N"/"S/N" = sin número) | | `unit` | `unit` (if non-empty) | | `postcode`|
|
|
30
|
+
* `postcode` | | `locality`| `locality` (Overture address_levels municipality, or postal_city) |
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { TextSpliterator } from "spliterator"
|
|
34
|
+
|
|
35
|
+
import { stableSourceID } from "../../adapter.ts"
|
|
36
|
+
import { formatAddress, reconcileComponents } from "../../format.ts"
|
|
37
|
+
import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
|
|
38
|
+
|
|
39
|
+
export const OVERTURE_ADAPTER_ID = "overture"
|
|
40
|
+
export const OVERTURE_DEFAULT_LICENSE = "CDLA-Permissive-2.0"
|
|
41
|
+
|
|
42
|
+
/** The flattened per-row shape emitted by `ingest-overture-addresses.ts --corpus-jsonl`. */
|
|
43
|
+
interface OvertureCorpusRow {
|
|
44
|
+
street?: string
|
|
45
|
+
number?: string
|
|
46
|
+
unit?: string
|
|
47
|
+
postcode?: string
|
|
48
|
+
locality?: string
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseLine(line: string): OvertureCorpusRow | null {
|
|
52
|
+
const t = line.trim()
|
|
53
|
+
|
|
54
|
+
if (!t || t.startsWith("#")) return null
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const o = JSON.parse(t)
|
|
58
|
+
|
|
59
|
+
return o && typeof o === "object" ? (o as OvertureCorpusRow) : null
|
|
60
|
+
} catch {
|
|
61
|
+
return null
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function createOvertureAdapter(): CorpusAdapter {
|
|
66
|
+
return {
|
|
67
|
+
id: OVERTURE_ADAPTER_ID,
|
|
68
|
+
defaultLicense: OVERTURE_DEFAULT_LICENSE,
|
|
69
|
+
description: "Overture Maps Addresses (global): per-country JSONL of street/number/postcode/locality.",
|
|
70
|
+
|
|
71
|
+
async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
|
|
72
|
+
if (!opts.country) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"overture adapter: --country is required (the Overture JSONL is per-country and rows omit a country field)"
|
|
75
|
+
)
|
|
76
|
+
}
|
|
77
|
+
const country = opts.country
|
|
78
|
+
|
|
79
|
+
// TextSpliterator streams string lines (parseLine keeps tolerating blank/`#`/malformed
|
|
80
|
+
// lines by returning null); the path string lets the lib own + dispose the file handle,
|
|
81
|
+
// including on an early `break`.
|
|
82
|
+
const lines = TextSpliterator.fromAsync(opts.inputPath)
|
|
83
|
+
|
|
84
|
+
let emitted = 0
|
|
85
|
+
|
|
86
|
+
for await (const line of lines) {
|
|
87
|
+
if (opts.signal?.aborted) break
|
|
88
|
+
|
|
89
|
+
if (opts.limit !== undefined && emitted >= opts.limit) break
|
|
90
|
+
|
|
91
|
+
const r = parseLine(line)
|
|
92
|
+
|
|
93
|
+
if (!r) continue
|
|
94
|
+
|
|
95
|
+
const street = r.street?.trim() ?? ""
|
|
96
|
+
const number = r.number?.trim() ?? ""
|
|
97
|
+
const unit = r.unit?.trim() ?? ""
|
|
98
|
+
const postcode = r.postcode?.trim() ?? ""
|
|
99
|
+
const locality = r.locality?.trim() ?? ""
|
|
100
|
+
|
|
101
|
+
// Only useful with a street + (postcode OR locality); point-only rows quarantine anyway.
|
|
102
|
+
if (!street) continue
|
|
103
|
+
|
|
104
|
+
if (!postcode && !locality) continue
|
|
105
|
+
|
|
106
|
+
const components: CanonicalRow["components"] = {}
|
|
107
|
+
|
|
108
|
+
// Overture "S-N" / "S/N" = sin número; only keep a real numeric house number.
|
|
109
|
+
if (/^\d/.test(number)) {
|
|
110
|
+
components.house_number = number
|
|
111
|
+
}
|
|
112
|
+
components.street = street
|
|
113
|
+
|
|
114
|
+
if (unit) {
|
|
115
|
+
components.unit = unit
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (postcode) {
|
|
119
|
+
components.postcode = postcode
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (locality) {
|
|
123
|
+
components.locality = locality
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const raw = formatAddress(components, country, { separator: ", " })
|
|
127
|
+
|
|
128
|
+
if (!raw) continue
|
|
129
|
+
|
|
130
|
+
const aligned = reconcileComponents(components, raw)
|
|
131
|
+
|
|
132
|
+
if (Object.keys(aligned).length === 0) continue
|
|
133
|
+
|
|
134
|
+
yield {
|
|
135
|
+
raw,
|
|
136
|
+
components: aligned,
|
|
137
|
+
country,
|
|
138
|
+
source: OVERTURE_ADAPTER_ID,
|
|
139
|
+
source_id: stableSourceID(OVERTURE_ADAPTER_ID, aligned),
|
|
140
|
+
corpus_version: "",
|
|
141
|
+
license: OVERTURE_DEFAULT_LICENSE,
|
|
142
|
+
}
|
|
143
|
+
emitted++
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export const overtureAdapter = createOvertureAdapter()
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `state-hi-schools`: Hawaii DOE public + charter schools CSV consumer.
|
|
7
|
+
*
|
|
8
|
+
* The Hawaii State Department of Education publishes a directory of all HIDOE schools and public
|
|
9
|
+
* charter schools (PCS) as an XLSX workbook (`SchoolList.xlsx`) with two sheets: `HIDOE` (~258
|
|
10
|
+
* rows) and `PCS` (~38 rows). Total ~296 rows statewide. Each row carries a school name,
|
|
11
|
+
* single-line street address, city, ZIP, a numeric `code`, and HI-specific administrative columns
|
|
12
|
+
* (complex, complex_area, district, island, charter).
|
|
13
|
+
*
|
|
14
|
+
* The adapter consumes a flat CSV the operator pre-builds via `fetch-state-hi-schools.ts`, which
|
|
15
|
+
* concatenates both sheets under one shared header. Column names match the workbook header
|
|
16
|
+
* verbatim (lower-snake-case: `code`, `name`, `address`, `city`, `zip`, ...).
|
|
17
|
+
*
|
|
18
|
+
* Address parsing notes: Hawaii's residential numbering is hyphenated on Oahu (`47-470 Hui Aeko
|
|
19
|
+
* Place`), Kauai (`2-4035 Kaumualii Hwy`), and elsewhere. The shared HOUSE_NUMBER_PREFIX regex
|
|
20
|
+
* covers this via its optional `(?:-\d+)?` group.
|
|
21
|
+
*
|
|
22
|
+
* The `island` and `district` columns are HIDOE administrative labels (Honolulu, Central, Leeward,
|
|
23
|
+
* Windward, Hilo, Hawaii, Maui, Kauai) — they are NOT US counties and intentionally are not
|
|
24
|
+
* surfaced as `subregion`.
|
|
25
|
+
*
|
|
26
|
+
* Output: one row per school with `venue` (school name), `(house_number?, street, locality,
|
|
27
|
+
* region=HI, postcode)`, and a stable `source_id` derived from the school `code`.
|
|
28
|
+
*
|
|
29
|
+
* License: stamped `"Public Domain"` per Hawaii state government open-data terms.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { createReadStream } from "node:fs"
|
|
33
|
+
|
|
34
|
+
import { parse as csvParse } from "csv-parse"
|
|
35
|
+
|
|
36
|
+
import { stableSourceID } from "../../adapter.ts"
|
|
37
|
+
import { lookupStateAbbreviation } from "../../codex/us-fips-state.ts"
|
|
38
|
+
import { reconcileComponents } from "../../format.ts"
|
|
39
|
+
import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
|
|
40
|
+
|
|
41
|
+
export const STATE_HI_SCHOOLS_ADAPTER_ID = "state-hi-schools"
|
|
42
|
+
export const STATE_HI_SCHOOLS_DEFAULT_LICENSE = "Public Domain"
|
|
43
|
+
|
|
44
|
+
const HOUSE_NUMBER_PREFIX = /^(\d+(?:-\d+)?[A-Za-z]?)\s+(.+)$/
|
|
45
|
+
const HI_STATE_ABBR = "HI"
|
|
46
|
+
|
|
47
|
+
interface HiSchoolRow {
|
|
48
|
+
code: string
|
|
49
|
+
name: string
|
|
50
|
+
address: string
|
|
51
|
+
city: string
|
|
52
|
+
zip: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function splitAddress(address: string): { house_number?: string; street: string } | null {
|
|
56
|
+
const trimmed = address.trim()
|
|
57
|
+
|
|
58
|
+
if (!trimmed) return null
|
|
59
|
+
const m = HOUSE_NUMBER_PREFIX.exec(trimmed)
|
|
60
|
+
|
|
61
|
+
if (m) return { house_number: m[1], street: m[2]!.trim() }
|
|
62
|
+
|
|
63
|
+
return { street: trimmed }
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function normalizeZip(raw: string): string {
|
|
67
|
+
const trimmed = raw.trim()
|
|
68
|
+
|
|
69
|
+
if (!trimmed) return ""
|
|
70
|
+
|
|
71
|
+
// XLSX → CSV conversion may emit numeric ZIPs without leading zeros. HI ZIPs all begin
|
|
72
|
+
// with 96, so a 4-digit value indicates a leading-zero stripped during numeric coercion
|
|
73
|
+
// (defensive — has not been observed in the published file as of 2026-05).
|
|
74
|
+
if (/^\d{4}$/.test(trimmed)) return `0${trimmed}`
|
|
75
|
+
|
|
76
|
+
return trimmed
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export function createStateHiSchoolsAdapter(): CorpusAdapter {
|
|
80
|
+
return {
|
|
81
|
+
id: STATE_HI_SCHOOLS_ADAPTER_ID,
|
|
82
|
+
defaultLicense: STATE_HI_SCHOOLS_DEFAULT_LICENSE,
|
|
83
|
+
description: "Hawaii DOE School Directory — ~300 K-12 public + charter schools with venue+address (public-domain).",
|
|
84
|
+
|
|
85
|
+
async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
|
|
86
|
+
if (opts.country && opts.country !== "US") {
|
|
87
|
+
throw new Error(`state-hi-schools adapter: only US supported, got country=${opts.country}`)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
|
|
91
|
+
const parser = stream.pipe(
|
|
92
|
+
csvParse({
|
|
93
|
+
columns: true,
|
|
94
|
+
skip_empty_lines: true,
|
|
95
|
+
relax_quotes: true,
|
|
96
|
+
relax_column_count: true,
|
|
97
|
+
})
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const state = lookupStateAbbreviation(HI_STATE_ABBR)
|
|
101
|
+
|
|
102
|
+
if (!state) {
|
|
103
|
+
throw new Error(`state-hi-schools adapter: HI not found in state codex (corpus bug)`)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let emitted = 0
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
for await (const record of parser as AsyncIterable<HiSchoolRow>) {
|
|
110
|
+
if (opts.signal?.aborted) break
|
|
111
|
+
|
|
112
|
+
if (opts.limit !== undefined && emitted >= opts.limit) break
|
|
113
|
+
|
|
114
|
+
const name = (record.name ?? "").trim()
|
|
115
|
+
const address = (record.address ?? "").trim()
|
|
116
|
+
const city = (record.city ?? "").trim()
|
|
117
|
+
const zip = normalizeZip(record.zip ?? "")
|
|
118
|
+
|
|
119
|
+
if (!name || !address || !city || !zip) continue
|
|
120
|
+
|
|
121
|
+
const split = splitAddress(address)
|
|
122
|
+
|
|
123
|
+
if (!split) continue
|
|
124
|
+
|
|
125
|
+
const components: CanonicalRow["components"] = {
|
|
126
|
+
venue: name,
|
|
127
|
+
...(split.house_number ? { house_number: split.house_number } : {}),
|
|
128
|
+
street: split.street,
|
|
129
|
+
locality: city,
|
|
130
|
+
region: state.abbreviation,
|
|
131
|
+
postcode: zip,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const streetPart = [split.house_number, split.street].filter(Boolean).join(" ").trim()
|
|
135
|
+
const raw = [
|
|
136
|
+
name,
|
|
137
|
+
streetPart,
|
|
138
|
+
[city, [HI_STATE_ABBR, zip].filter(Boolean).join(" ")].filter(Boolean).join(", "),
|
|
139
|
+
]
|
|
140
|
+
.filter(Boolean)
|
|
141
|
+
.join(", ")
|
|
142
|
+
|
|
143
|
+
const aligned = reconcileComponents(components, raw)
|
|
144
|
+
|
|
145
|
+
if (Object.keys(aligned).length <= 2) continue
|
|
146
|
+
|
|
147
|
+
const code = (record.code ?? "").toString().trim()
|
|
148
|
+
const sourceID = code
|
|
149
|
+
? `${STATE_HI_SCHOOLS_ADAPTER_ID}-${code}`
|
|
150
|
+
: stableSourceID(STATE_HI_SCHOOLS_ADAPTER_ID, aligned)
|
|
151
|
+
|
|
152
|
+
yield {
|
|
153
|
+
raw,
|
|
154
|
+
components: aligned,
|
|
155
|
+
country: "US",
|
|
156
|
+
locale: "en-US",
|
|
157
|
+
source: STATE_HI_SCHOOLS_ADAPTER_ID,
|
|
158
|
+
source_id: sourceID,
|
|
159
|
+
corpus_version: "",
|
|
160
|
+
license: STATE_HI_SCHOOLS_DEFAULT_LICENSE,
|
|
161
|
+
}
|
|
162
|
+
emitted++
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
stream.destroy()
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const stateHiSchoolsAdapter = createStateHiSchoolsAdapter()
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `state-ia-contractors`: Iowa Active Construction Contractor Registrations CSV consumer.
|
|
7
|
+
*
|
|
8
|
+
* Iowa Workforce Development publishes a public registry of active construction contractors. Each
|
|
9
|
+
* row carries a business name, street address, city/state/zip, and contact info.
|
|
10
|
+
*
|
|
11
|
+
* The adapter consumes the CSV the operator pre-downloads via `fetch-state-sources.ts`.
|
|
12
|
+
*
|
|
13
|
+
* Output: one row per contractor with `venue` (business name) and address quad `(house_number,
|
|
14
|
+
* street, locality, region, postcode)`.
|
|
15
|
+
*
|
|
16
|
+
* License: stamped `"Public Domain"` per Iowa state government open-data terms.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { createReadStream } from "node:fs"
|
|
20
|
+
|
|
21
|
+
import { parse as csvParse } from "csv-parse"
|
|
22
|
+
|
|
23
|
+
import { stableSourceID } from "../../adapter.ts"
|
|
24
|
+
import { lookupStateAbbreviation } from "../../codex/us-fips-state.ts"
|
|
25
|
+
import { reconcileComponents } from "../../format.ts"
|
|
26
|
+
import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
|
|
27
|
+
|
|
28
|
+
export const STATE_IA_CONTRACTORS_ADAPTER_ID = "state-ia-contractors"
|
|
29
|
+
export const STATE_IA_CONTRACTORS_DEFAULT_LICENSE = "Public Domain"
|
|
30
|
+
|
|
31
|
+
const HOUSE_NUMBER_PREFIX = /^(\d+(?:-\d+)?[A-Za-z]?)\s+(.+)$/
|
|
32
|
+
|
|
33
|
+
interface IaContractorRow {
|
|
34
|
+
"Registration #": string
|
|
35
|
+
"Business Name": string
|
|
36
|
+
"Address 1": string
|
|
37
|
+
"Address 2": string
|
|
38
|
+
City: string
|
|
39
|
+
State: string
|
|
40
|
+
"Zip Code": string
|
|
41
|
+
"First Name": string
|
|
42
|
+
"Last Name": string
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function splitAddress(address: string): { house_number?: string; street: string } | null {
|
|
46
|
+
const trimmed = address.trim()
|
|
47
|
+
|
|
48
|
+
if (!trimmed) return null
|
|
49
|
+
const m = HOUSE_NUMBER_PREFIX.exec(trimmed)
|
|
50
|
+
|
|
51
|
+
if (m) return { house_number: m[1], street: m[2]!.trim() }
|
|
52
|
+
|
|
53
|
+
return { street: trimmed }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function createStateIaContractorsAdapter(): CorpusAdapter {
|
|
57
|
+
return {
|
|
58
|
+
id: STATE_IA_CONTRACTORS_ADAPTER_ID,
|
|
59
|
+
defaultLicense: STATE_IA_CONTRACTORS_DEFAULT_LICENSE,
|
|
60
|
+
description:
|
|
61
|
+
"Iowa Active Construction Contractor Registrations — business name + full street address (public-domain).",
|
|
62
|
+
|
|
63
|
+
async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
|
|
64
|
+
if (opts.country && opts.country !== "US") {
|
|
65
|
+
throw new Error(`state-ia-contractors adapter: only US supported, got country=${opts.country}`)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
|
|
69
|
+
const parser = stream.pipe(
|
|
70
|
+
csvParse({
|
|
71
|
+
columns: true,
|
|
72
|
+
skip_empty_lines: true,
|
|
73
|
+
relax_quotes: true,
|
|
74
|
+
relax_column_count: true,
|
|
75
|
+
})
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
let emitted = 0
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
for await (const record of parser as AsyncIterable<IaContractorRow>) {
|
|
82
|
+
if (opts.signal?.aborted) break
|
|
83
|
+
|
|
84
|
+
if (opts.limit !== undefined && emitted >= opts.limit) break
|
|
85
|
+
|
|
86
|
+
const businessName = (record["Business Name"] ?? "").trim()
|
|
87
|
+
const address1 = (record["Address 1"] ?? "").trim()
|
|
88
|
+
const address2 = (record["Address 2"] ?? "").trim()
|
|
89
|
+
const city = (record.City ?? "").trim()
|
|
90
|
+
const stateAbbr = (record.State ?? "").trim()
|
|
91
|
+
const zip = (record["Zip Code"] ?? "").trim()
|
|
92
|
+
|
|
93
|
+
if (!city || !zip) continue
|
|
94
|
+
|
|
95
|
+
const state = lookupStateAbbreviation(stateAbbr)
|
|
96
|
+
|
|
97
|
+
if (!state) continue
|
|
98
|
+
|
|
99
|
+
const fullAddress = [address1, address2].filter(Boolean).join(" ")
|
|
100
|
+
const split = splitAddress(fullAddress)
|
|
101
|
+
|
|
102
|
+
if (!split) continue
|
|
103
|
+
|
|
104
|
+
const venue = businessName || undefined
|
|
105
|
+
|
|
106
|
+
const components: CanonicalRow["components"] = {
|
|
107
|
+
...(venue ? { venue } : {}),
|
|
108
|
+
...(split.house_number ? { house_number: split.house_number } : {}),
|
|
109
|
+
street: split.street,
|
|
110
|
+
locality: city,
|
|
111
|
+
region: state.abbreviation,
|
|
112
|
+
postcode: zip,
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const streetPart = [split.house_number, split.street].filter(Boolean).join(" ").trim()
|
|
116
|
+
const raw = [venue, streetPart, [city, [stateAbbr, zip].filter(Boolean).join(" ")].filter(Boolean).join(", ")]
|
|
117
|
+
.filter(Boolean)
|
|
118
|
+
.join(", ")
|
|
119
|
+
|
|
120
|
+
const aligned = reconcileComponents(components, raw)
|
|
121
|
+
|
|
122
|
+
if (Object.keys(aligned).length <= 2) continue
|
|
123
|
+
|
|
124
|
+
const regNum = (record["Registration #"] ?? "").trim()
|
|
125
|
+
const sourceID = regNum
|
|
126
|
+
? `${STATE_IA_CONTRACTORS_ADAPTER_ID}-${regNum}`
|
|
127
|
+
: stableSourceID(STATE_IA_CONTRACTORS_ADAPTER_ID, aligned)
|
|
128
|
+
|
|
129
|
+
yield {
|
|
130
|
+
raw,
|
|
131
|
+
components: aligned,
|
|
132
|
+
country: "US",
|
|
133
|
+
locale: "en-US",
|
|
134
|
+
source: STATE_IA_CONTRACTORS_ADAPTER_ID,
|
|
135
|
+
source_id: sourceID,
|
|
136
|
+
corpus_version: "",
|
|
137
|
+
license: STATE_IA_CONTRACTORS_DEFAULT_LICENSE,
|
|
138
|
+
}
|
|
139
|
+
emitted++
|
|
140
|
+
}
|
|
141
|
+
} finally {
|
|
142
|
+
stream.destroy()
|
|
143
|
+
}
|
|
144
|
+
},
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export const stateIaContractorsAdapter = createStateIaContractorsAdapter()
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
*
|
|
6
|
+
* `state-ny-notaries`: New York Commissioned Notaries CSV consumer.
|
|
7
|
+
*
|
|
8
|
+
* The New York Department of State publishes a registry of commissioned notaries public. Each row
|
|
9
|
+
* optionally carries a business name and business address (~1-5% fill rate).
|
|
10
|
+
*
|
|
11
|
+
* The adapter consumes the CSV the operator pre-downloads via `fetch-state-sources.ts`. Column
|
|
12
|
+
* names match the data.ny.gov export header (note: some columns have leading spaces).
|
|
13
|
+
*
|
|
14
|
+
* License: stamped `"Public Domain"` per New York state government open-data terms.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { createReadStream } from "node:fs"
|
|
18
|
+
|
|
19
|
+
import { parse as csvParse } from "csv-parse"
|
|
20
|
+
|
|
21
|
+
import { stableSourceID } from "../../adapter.ts"
|
|
22
|
+
import { lookupStateAbbreviation } from "../../codex/us-fips-state.ts"
|
|
23
|
+
import { reconcileComponents } from "../../format.ts"
|
|
24
|
+
import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "../../types.ts"
|
|
25
|
+
|
|
26
|
+
export const STATE_NY_NOTARIES_ADAPTER_ID = "state-ny-notaries"
|
|
27
|
+
export const STATE_NY_NOTARIES_DEFAULT_LICENSE = "Public Domain"
|
|
28
|
+
|
|
29
|
+
const HOUSE_NUMBER_PREFIX = /^(\d+(?:-\d+)?[A-Za-z]?)\s+(.+)$/
|
|
30
|
+
|
|
31
|
+
interface NyNotaryRow {
|
|
32
|
+
"Commission Holder Name": string
|
|
33
|
+
"Commission Number (UID)": string
|
|
34
|
+
"Business Name (if available)": string
|
|
35
|
+
"Business Address 1 (if available)": string
|
|
36
|
+
"Business Address 2 (if available)": string
|
|
37
|
+
" Business City (if available)": string
|
|
38
|
+
"Business State (if available)": string
|
|
39
|
+
"Business Zip (if available)": string
|
|
40
|
+
"Commissioned County": string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function splitAddress(address: string): { house_number?: string; street: string } | null {
|
|
44
|
+
const trimmed = address.trim()
|
|
45
|
+
|
|
46
|
+
if (!trimmed) return null
|
|
47
|
+
const m = HOUSE_NUMBER_PREFIX.exec(trimmed)
|
|
48
|
+
|
|
49
|
+
if (m) return { house_number: m[1], street: m[2]!.trim() }
|
|
50
|
+
|
|
51
|
+
return { street: trimmed }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const RAW_NY_COLUMNS = [
|
|
55
|
+
"Commission Holder Name",
|
|
56
|
+
"Commission Number (UID)",
|
|
57
|
+
"Business Name (if available)",
|
|
58
|
+
"Business Address 1 (if available)",
|
|
59
|
+
"Business Address 2 (if available)",
|
|
60
|
+
" Business City (if available)",
|
|
61
|
+
"Business State (if available)",
|
|
62
|
+
"Business Zip (if available)",
|
|
63
|
+
"Commissioned County",
|
|
64
|
+
] as const
|
|
65
|
+
|
|
66
|
+
export function createStateNyNotariesAdapter(): CorpusAdapter {
|
|
67
|
+
return {
|
|
68
|
+
id: STATE_NY_NOTARIES_ADAPTER_ID,
|
|
69
|
+
defaultLicense: STATE_NY_NOTARIES_DEFAULT_LICENSE,
|
|
70
|
+
description: "New York Commissioned Notaries — name + optional business address (public-domain).",
|
|
71
|
+
|
|
72
|
+
async *rows(opts: AdapterOptions): AsyncIterable<CanonicalRow> {
|
|
73
|
+
if (opts.country && opts.country !== "US") {
|
|
74
|
+
throw new Error(`state-ny-notaries adapter: only US supported, got country=${opts.country}`)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const stream = createReadStream(opts.inputPath, { encoding: "utf8" })
|
|
78
|
+
const parser = stream.pipe(
|
|
79
|
+
csvParse({
|
|
80
|
+
columns: true,
|
|
81
|
+
skip_empty_lines: true,
|
|
82
|
+
relax_quotes: true,
|
|
83
|
+
relax_column_count: true,
|
|
84
|
+
})
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
let emitted = 0
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
for await (const rawRecord of parser as AsyncIterable<Record<string, string>>) {
|
|
91
|
+
if (opts.signal?.aborted) break
|
|
92
|
+
|
|
93
|
+
if (opts.limit !== undefined && emitted >= opts.limit) break
|
|
94
|
+
|
|
95
|
+
// NY CSV has columns with leading spaces, so we normalize by trimming keys.
|
|
96
|
+
const record: Record<string, string> = {}
|
|
97
|
+
|
|
98
|
+
for (const key of Object.keys(rawRecord)) {
|
|
99
|
+
record[key.trim()] = rawRecord[key] ?? ""
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const holderName = (record["Commission Holder Name"] ?? "").trim()
|
|
103
|
+
const businessName = (record["Business Name (if available)"] ?? "").trim()
|
|
104
|
+
const address1 = (record["Business Address 1 (if available)"] ?? "").trim()
|
|
105
|
+
const address2 = (record["Business Address 2 (if available)"] ?? "").trim()
|
|
106
|
+
const city = (record["Business City (if available)"] ?? "").trim()
|
|
107
|
+
const stateAbbr = (record["Business State (if available)"] ?? "").trim()
|
|
108
|
+
const zip = (record["Business Zip (if available)"] ?? "").trim()
|
|
109
|
+
const county = (record["Commissioned County"] ?? "").trim()
|
|
110
|
+
|
|
111
|
+
if (!city || !stateAbbr || !zip) continue
|
|
112
|
+
|
|
113
|
+
if (!address1 && !address2) continue
|
|
114
|
+
|
|
115
|
+
const state = lookupStateAbbreviation(stateAbbr)
|
|
116
|
+
|
|
117
|
+
if (!state) continue
|
|
118
|
+
|
|
119
|
+
const fullAddress = [address1, address2].filter(Boolean).join(" ")
|
|
120
|
+
const split = splitAddress(fullAddress)
|
|
121
|
+
|
|
122
|
+
if (!split) continue
|
|
123
|
+
|
|
124
|
+
const venue = businessName || holderName || undefined
|
|
125
|
+
|
|
126
|
+
const components: CanonicalRow["components"] = {
|
|
127
|
+
...(venue ? { venue } : {}),
|
|
128
|
+
...(split.house_number ? { house_number: split.house_number } : {}),
|
|
129
|
+
street: split.street,
|
|
130
|
+
locality: city,
|
|
131
|
+
region: state.abbreviation,
|
|
132
|
+
postcode: zip,
|
|
133
|
+
...(county ? { subregion: county } : {}),
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const streetPart = [split.house_number, split.street].filter(Boolean).join(" ").trim()
|
|
137
|
+
const raw = [venue, streetPart, [city, [stateAbbr, zip].filter(Boolean).join(" ")].filter(Boolean).join(", ")]
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.join(", ")
|
|
140
|
+
|
|
141
|
+
const aligned = reconcileComponents(components, raw)
|
|
142
|
+
|
|
143
|
+
if (Object.keys(aligned).length <= 2) continue
|
|
144
|
+
|
|
145
|
+
const commNum = (record["Commission Number (UID)"] ?? "").trim()
|
|
146
|
+
const sourceID = commNum
|
|
147
|
+
? `${STATE_NY_NOTARIES_ADAPTER_ID}-${commNum}`
|
|
148
|
+
: stableSourceID(STATE_NY_NOTARIES_ADAPTER_ID, aligned)
|
|
149
|
+
|
|
150
|
+
yield {
|
|
151
|
+
raw,
|
|
152
|
+
components: aligned,
|
|
153
|
+
country: "US",
|
|
154
|
+
locale: "en-US",
|
|
155
|
+
source: STATE_NY_NOTARIES_ADAPTER_ID,
|
|
156
|
+
source_id: sourceID,
|
|
157
|
+
corpus_version: "",
|
|
158
|
+
license: STATE_NY_NOTARIES_DEFAULT_LICENSE,
|
|
159
|
+
}
|
|
160
|
+
emitted++
|
|
161
|
+
}
|
|
162
|
+
} finally {
|
|
163
|
+
stream.destroy()
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
export const stateNyNotariesAdapter = createStateNyNotariesAdapter()
|