/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * `state-tx-notaries`: Texas Notary Public Commissions CSV consumer. * * The Texas Secretary of State publishes a registry of commissioned notaries public. Each row * optionally carries a mailing address in free-form text (often multi-line with embedded * city/state/zip). Address fill rate is ~5-10%. * * The adapter parses the embedded `Address` field for city/state/zip using a trailing `"CITY, ST * ZIP"` pattern. * * License: stamped `"Public Domain"` per Texas state government open-data terms. */ import { splitStreetLine, stableSourceID } from "@mailwoman/corpus/adapters/utils" import { lookupStateAbbreviation } from "@mailwoman/corpus/codex/us-fips-state" import type { AdapterOptions, CanonicalRow, CorpusAdapter } from "@mailwoman/corpus/types" import { reconcileComponents } from "@mailwoman/formatter" import { CSVSpliterator } from "spliterator" /** * Registry id for this adapter. Stamped into every row it emits, so a corpus record can be traced back to the dataset * it came from. */ export const STATE_TX_NOTARIES_ADAPTER_ID = "state-tx-notaries" /** * License carried by this source (Public Domain), attached to each row so downstream consumers inherit the terms rather * than having to look them up. */ export const STATE_TX_NOTARIES_DEFAULT_LICENSE = "Public Domain" /** * Match trailing "CITY, ST ZIP" or "CITY, ST" at the end of an address line. */ const CITY_STATE_ZIP_SUFFIX = /[,]?\s*([^,]+),\s*([A-Z]{2})\s*(\d{5}(?:-\d{4})?)?\s*$/i interface TxNotaryRow { "Notary ID": string "First Name": string "Last Name": string Address: string } export function createStateTxNotariesAdapter(): CorpusAdapter { return { id: STATE_TX_NOTARIES_ADAPTER_ID, defaultLicense: STATE_TX_NOTARIES_DEFAULT_LICENSE, description: "Texas Notary Public Commissions — name + mailing address with embedded city/state/zip (public-domain).", async *rows(opts: AdapterOptions): AsyncIterable { if (opts.country && opts.country !== "US") { throw new Error(`state-tx-notaries adapter: only US supported, got country=${opts.country}`) } const rows = CSVSpliterator.fromAsync(opts.inputPath, { mode: "object", normalizeKeys: false, enableQuoteHandling: true, }) let emitted = 0 for await (const record of rows as AsyncIterable) { if (opts.signal?.aborted) break if (opts.limit !== undefined && emitted >= opts.limit) break const rawAddress = (record.Address ?? "").trim() if (!rawAddress) continue const firstName = (record["First Name"] ?? "").trim() const lastName = (record["Last Name"] ?? "").trim() const notaryID = (record["Notary ID"] ?? "").trim() // Parse embedded city/state/zip from the trailing portion of the address. // Addresses look like: "1215 MCMILLAN DR\nCEDAR HILL, TX 75104" const addrSingleLine = rawAddress.replaceAll("\n", ", ") const cszMatch = CITY_STATE_ZIP_SUFFIX.exec(addrSingleLine) if (!cszMatch) continue const city = (cszMatch[1] ?? "").trim() const stateAbbr = (cszMatch[2] ?? "").trim() const zip = (cszMatch[3] ?? "").trim() if (!city || !stateAbbr) continue const state = lookupStateAbbreviation(stateAbbr) if (!state) continue // Extract the street portion (everything before the city/state/zip) const streetPortion = addrSingleLine.slice(0, cszMatch.index).replace(/,\s*$/, "").trim() if (!streetPortion) continue const split = splitStreetLine(streetPortion) if (!split) continue const venue = [firstName, lastName].filter(Boolean).join(" ") || undefined const components: CanonicalRow["components"] = { ...(venue ? { venue } : {}), ...(split.house_number ? { house_number: split.house_number } : {}), street: split.street, locality: city, region: state.abbreviation, ...(zip ? { postcode: zip } : {}), } const streetPart = [split.house_number, split.street].filter(Boolean).join(" ").trim() const raw = [venue, streetPart, [city, [stateAbbr, zip].filter(Boolean).join(" ")].filter(Boolean).join(", ")] .filter(Boolean) .join(", ") const aligned = reconcileComponents(components, raw) if (Object.keys(aligned).length <= 2) continue const sourceID = notaryID ? `${STATE_TX_NOTARIES_ADAPTER_ID}-${notaryID}` : stableSourceID(STATE_TX_NOTARIES_ADAPTER_ID, aligned) yield { raw, components: aligned, country: "US", locale: "en-US", source: STATE_TX_NOTARIES_ADAPTER_ID, source_id: sourceID, corpus_version: "", license: STATE_TX_NOTARIES_DEFAULT_LICENSE, } emitted++ } }, } } /** * The configured adapter instance registered with the corpus builder. */ export const stateTxNotariesAdapter = createStateTxNotariesAdapter()