/** * @fileoverview Geography resolution service. Converts place names and addresses to * Census FIPS codes using TIGERweb MapServer REST API and Census Geocoder. * @module services/geography/geography-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { GeographyType, ResolvedGeography } from './types.js'; export declare class GeographyService { /** * Resolve a place name or address to Census FIPS identifiers. * * Without an explicit `geographyType` the name is matched against an ordered chain of * layers (see {@link GeographyService.detectGeographyTypes}); a layer with no rows falls * through to the next, and so does one whose rows only contain the name — "King, WA" finds * "Kingston CDP" on the place level, which yields to "King County" named exactly that, and * is answered only when no later level has an exact row. A place level whose exact rows are * all CDPs yields the same way to an exact county, while still outranking a row that only * contains the name (see {@link GeographyService.preferredLevel}). The name's state suffix is * split once, before any query. Every level's name is tried as given first, so a real name on a * later level beats a respelling on an earlier one; only when no level has a row are the * respellings tried ({@link GeographyService.retryTerms}), and only an exhausted chain is a * `no_match`. * * A `countyFips` restricts that chain to the levels whose layers carry `COUNTY`, so the * scope is either applied or reported — never carried past a layer that cannot express it. */ resolveGeography(params: { name: string; geographyType?: GeographyType; countyFips?: string; }, ctx: Context): Promise; /** * Walk the chain's levels in order and pick the match that answers the name: the first level * with an exact row that is not only CDPs, else the first exact CDP match, else the first level * whose rows only contain the name. Levels after the answer are never queried. */ private preferredLevel; /** * Build the `county_scope_unsupported` error for a `county_fips` no level reached by this * call can apply — only `county` and `tract` sit inside a county, and a street address * resolves to a single point rather than a set a county could narrow. */ private countyScopeUnsupported; /** * Query one geography level for the name as given. Resolves to `null` when its layers have * no row for it. */ private resolvePrimary; /** * The respellings tried on a level once no level had a row for the name as given: the Saint * token in TIGERweb's forms, then the name with its accent-bearing letters wildcarded, then — * on the statistical-area levels — the name's leading city alone. Levels whose vocabulary * carries none of these get no retry. * * OMB renames a CBSA or CSA by its trailing cities ("Denver-Aurora-Lakewood" became * "Denver-Aurora-Centennial"), and every CBSA renamed between the 2020 and 2023 delineations * kept its leading city, so the text before the first hyphen still finds the area. A merged * CSA is found the same way, through the area that now contains the city. */ private retryTerms; /** * Query one geography level for one spelling of the name. Matching is case-insensitive — * TIGERweb's `LIKE` is not, so both sides are uppercased — and scoped by the state suffix * and county the caller gave, wherever the layer can express them. */ private resolveTerm; /** * Fetch TIGERweb features across a level's layers and narrow them to the rows that name the * term exactly, when any do. Resolves to `null` when the level has no row for the term. */ private findMatches; /** Resolve a level's match to its geography. */ private settle; /** * The 3-digit county prefix of an economic place's code: the one county the place lies in, or * `000` when it spans several. Resolves to `undefined` when no 2022 boundary locates it. * * A balance of county is coded `98` plus its county, so its prefix is in the code. An * incorporated place or CDP is tested against the counties whose interiors its own interior * meets (DE-9IM `T********`), so a county it only borders never counts. A layer-2 economic * place with no place boundary is a minor civil division — a New Jersey township, a New England * town — and a county subdivision nests in one county, which its row names. */ private economicPlaceCounty; /** * Build the `no_match` error for an economic place whose county prefix no 2022 boundary could * supply — its code cannot be completed, and a guessed prefix would query a different place. */ private economicPlaceUnplaced; /** Query one TIGERweb layer by WHERE clause. */ private queryLayer; /** Send one TIGERweb request, retrying transient failures, and read its JSON body. */ private requestTigerweb; /** * Read one code off a TIGERweb row, zero-padded to its documented width. * * TIGERweb drops leading zeros on some numeric fields — an unpadded `5` for Arkansas would * miss every FIPS lookup keyed on `05`. */ private levelCode; /** * Narrow to rows whose name spans the state the caller named. * * A statistical area covers whatever states it covers, so its layer carries no `STATE` to * put in a WHERE clause — but its name ends in the hyphenated list of those states, which * means the named one can sit second or third rather than first: "Kansas City, MO" and * "Kansas City, KS" are both the MO-KS metro area. A row whose name yields no state list * fails the check, since an unverifiable row does not satisfy a scope that was asked for. */ private filterByStateInName; /** * Narrow to rows whose name is exactly the queried term, when any are. * * A `NAME LIKE '%term%'` query also matches longer names — "Kansas City" returns * "North Kansas City city" alongside "Kansas City city" — so without this the first * row TIGERweb happens to return can be a different place than the one asked for. * * A row whose own name (`BASENAME`) is the term outranks one whose full `NAME` is: compared * case-insensitively, "Jersey City" is both Jersey City's `BASENAME` and the `NAME` "Jersey * city" of Jersey, GA, and only the first is the place the caller named. * * Among exact rows, an incorporated place outranks a census-designated place of the same name, * since a CDP is a statistical area and the government is what the name ordinarily means — * "Paradise, CA" is Paradise town, not the Paradise CDP in Mono County. A CDP is still reached * by its full name ("Paradise CDP, CA"). * * `exact` says whether the rows kept are named the term or only contain it, which is what * lets a later level's exact row outrank them. */ private preferExactMatches; /** * Build the `ambiguous_name` error, giving each candidate enough to act on without a * re-query: its own code, plus the state that separates same-named places. */ private ambiguousName; /** * Build the `no_match` error for a name no layer in the chain matched. The hint is built from * the split name, so it names the place and state apart and never appends a second state to * a name that already carries one. */ private noMatch; private noMatchHint; /** * Split a trailing state off a place name — "Seattle, WA", "Seattle, wa", "Chatham County, * Georgia", or a statistical area's "Omaha-Council Bluffs, NE-IA". * * After a comma, the whole final segment must be a state: a full name or territory alias * matched exactly and case-insensitively, or two-letter abbreviations in either case, * hyphenated into a list. Matching the whole segment keeps "McDowell County, West Virginia" * from reading as Virginia. Without a comma only an uppercase abbreviation splits, since a * trailing state word cannot be told apart from a place name ending in one. A list scopes by * its first state, the one the area is named for. */ private splitStateSuffix; private resolveAddress; /** * Ordered layer chain for a name with no explicit `geography_type`. * * A bare 5-digit ZIP (or ZIP+4) is a ZCTA — only codes are digits, and a caller holding a * place, county, or CBSA code has no reason to resolve it. Anything that is neither that, a * state name, nor a county/tract keyword is tried as a place * first and a county second, so "Seattle, WA" reaches the place layer while a name that * exists only as a county-equivalent still resolves. Keywords match whole words only — * "Marlborough" and "Parishville" are places — and "borough" tries the county layer before * the place layer, since it names an Alaska county-equivalent and a PA/NJ place alike. * A spelled-out state name must match * the whole input — a substring test would read "West Virginia University" as a state. * * "New York" resolves to the state: the Census place name for New York City is also * exactly "New York", and there is no qualifier a caller would naturally add to tell them * apart, so the city requires an explicit `geography_type: "place"`. */ private detectGeographyTypes; private looksLikeAddress; } export declare function initGeographyService(): void; export declare function getGeographyService(): GeographyService; //# sourceMappingURL=geography-service.d.ts.map