/** * @fileoverview Census Bureau Data API service. Handles data queries, response parsing, * and suppression code resolution for api.census.gov/data endpoints. * @module services/census-api/census-api-service */ import type { Context } from '@cyanheads/mcp-ts-core'; import type { CensusDataRow, CensusGeographyLevel, CensusPredicateValue, GeographyCheck } from './types.js'; /** * Zero-pad a fixed-width parent FIPS code to the width the Census API matches on, returning * undefined for a blank value so it reads as omitted. State codes are 2 digits and county codes * 3; the API compares them literally, so `state:5` finds nothing where `state:05` finds Arkansas. * * `*` is a scope, not a code — `in=state:53 county:*` is the only way to reach every block group * in a state — so it passes through untouched rather than becoming `00*`. */ export declare function padFips(value: string | undefined, width: number): string | undefined; /** * Canonicalize caller variable codes. Every supported dataset names its columns in uppercase and * the Census API matches `get=` case-sensitively — `b19013_001e` is a 400 — so uppercasing maps a * code onto the one spelling that answers, and the response is keyed by that spelling. A blank * code names no column, so it is dropped the way a blank scope input is, and a repeat is dropped * because the response is keyed by code and can hold each one once. */ export declare function normalizeVariableCodes(codes: readonly string[]): string[]; /** * Canonicalize a caller's predicate map. Keys are uppercased, since no supported dataset defines a * lowercase variable and `naics2017` has exactly one meaning. Values are trimmed, and a blank value * is dropped: the Census API reads `NAICS2017=` as a group-by over every category, the same as * `*`, while a blank is what a form-based client sends for a field it left empty — and "blank is * treated as omitted" is the rule the scope inputs already follow. * * `*` stays. A per-category breakdown of one geography is a real query, so its dimensions are * returned as `wildcards` for the rows to be labelled with the category each one is. */ export declare function normalizePredicates(input: Record | undefined): { predicates: Record; wildcards: string[]; }; /** * A dimension a predicate set to `*`. The API echoes its code on every row; `labelColumn` is the * dimension's own `_LABEL`/`_DESC` attribute, requested alongside when the dataset publishes one. */ export interface WildcardColumn { code: string; labelColumn?: string; } /** Every family of column a data query can put in `get=`, beyond `NAME`. */ export interface QueryColumns { /** Label attribute per filter dimension the query left unset, keyed by dimension code. */ defaultLabelColumns?: Record; /** Flag column per measure, keyed by the measure's code (e.g. `{ RCPTOT: 'RCPTOT_F' }`). */ flagColumns?: Record; /** Label column per record dimension, keyed by dimension code; both are requested. */ recordColumns?: Record; /** The caller's variable codes. */ variables: string[]; /** Dimensions a predicate set to `*`; only their label columns are requested. */ wildcardColumns?: WildcardColumn[]; } /** * The `get=` list a query sends, in order. `queryData` builds its request from this and * `planQueryColumns` counts it, so the count cannot drift from what goes over the wire. * * Each column appears once. A caller can name a column the server adds anyway — `MONTH_DESC` on * `pep/charv`, a flag or label column — and sending it twice would spend a second slot of the * 50-column limit on nothing. */ export declare function getColumnsFor(columns: QueryColumns): string[]; /** Outcome of fitting a query's columns under `GET_COLUMN_LIMIT`. */ export type ColumnPlan = { status: 'over_limit'; /** Columns the query would need, counting every one the server adds. */ columnCount: number; /** Most variable codes this query can carry once the added columns are counted. */ maxVariables: number; /** The columns the server adds that count against the limit: NAME, label, and record columns. */ addedColumns: string[]; } | { status: 'ok'; /** Wildcard dimensions to send, each with its label column only when it fit. */ wildcardColumns: WildcardColumn[]; /** Flag columns that fit. */ flagColumns: Record; /** Wildcarded dimensions whose label column did not fit — their rows carry the code alone. */ unlabelledWildcards: string[]; /** Measures whose flag column did not fit — a withheld value among them cannot be told apart. */ uncheckedFlags: string[]; }; /** * Fit a query's columns under the Census API's 50-column `get=` limit. * * `NAME`, the caller's codes, and the default-label and record columns are sent whatever the * dataset, so they decide whether the query can run at all; past the limit, the plan says how many * codes it can carry instead. Wildcard label columns and then flag columns are extras: they take * whatever room is left, in that order, and the ones that do not fit are named rather than sent. * Adding them never turns a query that fit into one the API rejects. */ export declare function planQueryColumns(columns: QueryColumns): ColumnPlan; /** Word a query that would exceed the column limit, naming the real per-call maximum and why. */ export declare function describeColumnLimit(requested: number, plan: { maxVariables: number; addedColumns: string[]; }): string; /** * Word what the column limit left out of a query that still ran, or `undefined` when nothing was. * A missing flag column is the serious one: a value the Census withheld reads as a real `0` * without it, so the codes it covers are named. */ export declare function describeColumnBudget(plan: { uncheckedFlags: string[]; unlabelledWildcards: string[]; }): string | undefined; /** * The distinct values each record column took across a response, keyed by column code. A column * that took one value did not split anything; one that took several is why a geography came back * on several rows, and its codes are what a caller pins the record with. */ export declare function observeRecordValues(rows: CensusDataRow[]): Record>; export declare class CensusApiService { /** geography.json per dataset+year — immutable upstream, cached for the discovery TTL. */ private readonly geographyLevelsCache; /** Wildcard group-by enumerations per dataset+year+dimension+NAICS scope. */ private readonly predicateValuesCache; /** * Query a Census dataset for variables at a specific geography. * Returns parsed rows with sentinels and flags resolved. * * The caller is responsible for fitting the columns under `GET_COLUMN_LIMIT` first — * `planQueryColumns` says which optional columns fit. */ queryData(params: QueryColumns & { geographyLevel: string; geographyFips: string; /** State FIPS code — required for sub-state geography levels. */ parentFips?: string; /** County FIPS code — required when querying tracts or block groups within a specific county. */ countyFips?: string; /** 6-digit tract code — scopes a block-group or block query to one tract of the county. */ tractFips?: string; /** * Dataset-specific filter values sent as extra query parameters, keyed by variable code * (e.g. `{ NAICS2017: '5112' }`). Omitting one the dataset requires is not an error * upstream — the API returns the aggregate across that dimension instead. */ predicates?: Record; dataset: string; year: number; }, ctx: Context): Promise; /** * Check a geography level and its supplied parents against the dataset's own * geography.json before spending a data query on a request the Census API will reject. * * Returns `ok` when the dataset has no metadata for the year — the data call then * reports the real problem rather than this check guessing at one. */ checkGeography(params: { dataset: string; year: number; geographyLevel: string; /** The `for=` value — `*` relaxes the innermost required parent. */ geographyFips: string; /** State FIPS, when supplied by the caller. */ parentFips?: string; /** County FIPS, when supplied by the caller. */ countyFips?: string; /** Tract code, when supplied by the caller. */ tractFips?: string; }, ctx: Context): Promise; /** * Fetch the list of geography levels supported by a dataset+year from the Census API. * Cached in-memory per dataset+year with the discovery TTL. */ fetchGeographyLevels(dataset: string, year: number, ctx: Context): Promise; /** * Enumerate the codes a filter dimension accepts, by wildcarding it on the data endpoint. * * Setting `=*` turns the predicate into a group-by, so the response carries one row * per code the dimension takes for the scope queried. `variables.json` publishes a * `values.item` map for only two dimensions across the whole catalog, so this is the only * route to the rest. Cached per dataset+year+dimension+scope with the discovery TTL. * * The scope matters: on `ecnbasic` the codes `TAXSTAT` and `TYPOP` take are published per * industry, so an unscoped call returns only the all-establishments row and a `naicsScope` * is what makes the enumeration useful — and complete only for that industry. * * What lands in `get=` decides what the wildcard answers from. A dimension's own label column * can be served from the dataset's published value map, so on `dec/ddhca` a label-only request * hands back all 5,543 declared `POPGROUP` codes; naming a `measure` instead forces the read * against the data file, which answers with the 2,996 the dataset publishes rows for. */ fetchPredicateValues(params: { dataset: string; year: number; /** The filter dimension to enumerate (e.g. "EMPSZES"). */ code: string; /** Attribute column carrying each code's label (e.g. "EMPSZES_LABEL"), when published. */ labelAttribute?: string; /** * Measure variable to request instead of the label column, forcing the enumeration to * read the data file. Codes come back unlabelled, so this is for checking which codes the * dataset publishes rather than for building a list to show. */ measure?: string; /** NAICS dimension code and industry value to scope the enumeration by. */ naicsScope?: { code: string; value: string; }; }, ctx: Context): Promise; private parseResponse; } export declare function initCensusApiService(): void; export declare function getCensusApiService(): CensusApiService; //# sourceMappingURL=census-api-service.d.ts.map