/** * @copyright Sister Software * @license AGPL-3.0 * @author Teffen Ellis, et al. * * Decompose a US street name into Stage 3 components (street_prefix, street, street_suffix) from the * curated libpostal/en directionals and street_types dictionaries — the same ones the runtime * StreetPrefixClassifier and StreetSuffixClassifier use, so corpus labels and runtime classifications * agree on the vocabulary. */ import { loadLibpostalDictionary } from "#adapters/libpostal-dictionary" const [DIRECTIONALS, STREET_TYPES] = await Promise.all([ loadLibpostalDictionary("en", "directionals.txt"), loadLibpostalDictionary("en", "street_types.txt"), ]) export interface DecomposedStreet { prefix: string | null street: string suffix: string | null } /** * Decompose a US street name into prefix/name/suffix components. * * Conservative, emitting a prefix or suffix only for a clear directional or street-type keyword. * It returns the original as `street` when no pattern matches. */ export function decomposeStreet(fullname: string): DecomposedStreet { const trimmed = fullname.trim() if (!trimmed) return { prefix: null, street: "", suffix: null } const tokens = trimmed.split(/\s+/) if (tokens.length === 1) return { prefix: null, street: trimmed, suffix: null } const norm = (s: string) => s.toLowerCase().replace(/\.$/, "") let prefix: string | null = null let suffix: string | null = null let startIdx = 0 let endIdx = tokens.length if (DIRECTIONALS.has(norm(tokens[0]!)) && tokens.length >= 2) { prefix = tokens[0]! startIdx = 1 } const last = norm(tokens[endIdx - 1]!) const secondLast = endIdx >= 2 ? norm(tokens[endIdx - 2]!) : "" if (DIRECTIONALS.has(last) && STREET_TYPES.has(secondLast)) { suffix = tokens.slice(endIdx - 2, endIdx).join(" ") endIdx -= 2 } else if (STREET_TYPES.has(last) && endIdx - startIdx >= 2) { suffix = tokens[endIdx - 1]! endIdx -= 1 } else if (DIRECTIONALS.has(last) && endIdx - startIdx >= 2) { suffix = tokens[endIdx - 1]! endIdx -= 1 } const street = tokens.slice(startIdx, endIdx).join(" ").trim() if (!street) { return { prefix: null, street: trimmed, suffix: null } } return { prefix, street, suffix } }