diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 33820d04..6c5c95cd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: language: pygrep entry: \b([A-Za-z]+)[ \t]+\1\b types: [text] - exclude: ^tests/data/ + exclude: ^(tests/data/|dataretrieval/codes/_county_table\.py$) - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.16.5 diff --git a/CONTEXT.md b/CONTEXT.md index f76a9f7e..625b9a1a 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -143,6 +143,36 @@ a service names a thing `site-types` or `site_type_code`, that is its vocabulary and is reproduced rather than translated. Prose that is not about one particular adapter says *monitoring location*. +**State** — A US state, the District of Columbia, or one of the five +territories, as the place a monitoring location is in. + +*Domain term.* The services spell it differently: Water Data `state_name` +(`"Wisconsin"`) and `state_code` (`"55"`), the Water Data statistics service +`state_code` (`"US:55"`), NGWMN `state_name` on `sites` but a postal `state` +(`"WI"`) on `providers`, NWDC `stateCd:WI`, WQP `statecode`, NWIS `stateCd`. +Each adapter keeps those parameters. Where an adapter also takes `state`, that is +a *unified argument* and not a service's spelling. + +**County** — A county or county equivalent (parish, borough, census area, +independent city, municipio, Connecticut planning region) within a state. + +*Domain term.* A county's three-digit code repeats across states, so it is +identified by state and county together, and the services spell the pair +differently: Water Data `state_code` + `county_code` (`"55"` + `"025"`) or +`county_name` (`"Dane County"`), the statistics service one `county_code` +(`"US:55:025"`), NGWMN `state_name` + `county_name`, NWDC `countyCd:55025`, WQP +`countycode`, NWIS `countyCd`. Where an adapter also takes `county`, that is a +unified argument. + +**Unified argument** — A getter argument named by the package for a domain +concept, accepting every common spelling of it and sending each service the one +it filters on: `state` (a name, postal code, or FIPS code) and `county` (a +FIPS code, or a name with its state). It is added beside the service's own +parameters, never in their place; the rule is ADR 0013's. + +*Core term.* The argument names are the package's; the conversion tables are in +`dataretrieval.codes`. + **Metadata** — The second half of every getter's return: the request URL, the elapsed time, and the response headers. Describes the *retrieval*, not the data. diff --git a/NEWS.md b/NEWS.md index 3b6b389a..f20d8822 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,5 @@ +**10/01/2026:** **New:** a `county` argument, like `state`, on `waterdata.get_monitoring_locations()`, `waterdata.get_combined_metadata()`, `waterdata.get_stats_por()`, `waterdata.get_stats_date_range()`, `ngwmn.get_sites()`, and `nwdc.get_wateruse()`. It accepts a five-digit FIPS code (`"55025"` or `55025`), the statistics service's `"US:55:025"`, or a name or three-digit code with its state (`county="Dane", state="WI"`), and sends each service the fields it filters on: Water Data `state_code` + `county_code`, statistics `county_code="US:55:025"`, NGWMN `state_name` + `county_name`, NWDC `countyCd:55025`. With `county`, `state` names the counties' state rather than filtering separately. `dataretrieval.codes.to_county()` exposes the conversion; its table is the Water Data `counties` collection for the 50 states, DC, and the territories, and a live test compares the two. The native `county_code` / `county_name` parameters are unchanged, and passing one with `county` raises `ValueError`. Counties in several states are sent to the Water Data location collections as a CQL `filter` OR-ing (state, county) pairs, so they cannot be combined with your own `filter`; `ngwmn.get_sites()` takes counties from one state per call, and raises for Connecticut, whose NGWMN sites are still filed under the counties the state replaced with planning regions in 2022 (pass `county_name="Hartford County"` instead). WQP and legacy NWIS are unchanged, as they are for `state`. **Bug fix:** `ngwmn.get_sites(county_name=[...])` with more than one name returned no rows: NGWMN matches nothing for a comma-joined `county_name`. Multi-value `sites` filters are now POSTed as CQL2, as the Water Data location collections' are. **Behavior change:** `nwdc.get_wateruse(state=..., county=...)` no longer raises for naming two locations; the state qualifies the county, and a county outside it raises. An unknown county such as Connecticut's retired `"09003"` now raises locally instead of HTTP 400. + **10/01/2026:** **Behavior change:** the numeric columns of the `waterdata` OGC getters and `ngwmn` — `value`, `altitude`, `drainage_area`, and the other measurement columns — are always `float64`. They were `int64` whenever every value in a response happened to be whole, so the same column changed dtype between calls, breaking concatenation, schema checks, and typed stores (#428). **Behavior change:** a value in a numeric or datetime column that cannot be parsed still becomes `NaN`/`NaT`, but now emits a `UserWarning` naming the column and the count, so a parse failure is distinguishable from a missing value. Repeat the call with `convert_type=False` to see the raw values. Expect it from `waterdata.get_monitoring_locations()`: its `construction_date` holds partial dates (`1925`, `194805`, `19100000`) that a datetime column cannot represent, and they were already being dropped without notice — 7,306 of 24,089 values in a query for Maryland groundwater sites. **10/01/2026:** **Bug fix:** `waterdata.get_nearest_continuous` turned a missing target (`NaT`, `None`, `NaN`, or `""`) into a `(time >= 'nan' AND time <= 'nan')` clause and sent it to the service, including when only one entry of a list was missing. It now raises `ValueError` before any request, naming how many targets are missing and the position of the first. Targets pandas cannot parse now raise a `ValueError` that names `targets`, rather than pandas' message, which named no argument and suggested a `format=` the getter does not accept. **Bug fix:** the same getter's `window` was not validated: a missing window (`None`, `'NaT'`) failed with an `AttributeError` while the filter was built, and a negative one (`'-PT5M'`) inverted every bound, so the query matched nothing and returned an empty frame indistinguishable from a gap in the data. Both now raise `ValueError` naming `window`; a zero window remains an exact-match query. **Behavior change:** a bare-number `window` now raises `TypeError`; pandas read `window=450` as 450 nanoseconds, so it matched almost nothing. Pass a duration such as `window='PT450S'`. Numeric `targets` raise `TypeError` for the same reason: `targets=[1.5e9]` meant as epoch seconds was read as 1970-01-01T00:00:01.5Z. **Behavior change:** calls that previously sent a malformed filter now fail locally, so a caller passing targets with gaps must drop or fill them first. diff --git a/dataretrieval/codes/__init__.py b/dataretrieval/codes/__init__.py index d14aa942..bd9469e1 100644 --- a/dataretrieval/codes/__init__.py +++ b/dataretrieval/codes/__init__.py @@ -1,8 +1,10 @@ -"""Facade over the ``states`` and ``timezones`` lookup tables. +"""Facade over the ``states``, ``counties``, and ``timezones`` lookup tables. -Re-exports the state code maps, their normalizers (``to_state``, -``apply_state``), and the ``tz`` UTC-offset map. +Re-exports the state code maps and their normalizers (``to_state``, +``apply_state``), the county map and its normalizers (``to_county``, +``apply_county``), and the ``tz`` UTC-offset map. """ +from .counties import * from .states import * from .timezones import * diff --git a/dataretrieval/codes/_county_table.py b/dataretrieval/codes/_county_table.py new file mode 100644 index 00000000..7e173ecd --- /dev/null +++ b/dataretrieval/codes/_county_table.py @@ -0,0 +1,3253 @@ +"""The county table behind :mod:`dataretrieval.codes.counties`: data only. + +Generated from the Water Data ``counties`` collection; see +``tests/counties_test.py::test_the_table_matches_the_water_data_counties_collection`` +for how to regenerate it. Kept apart from the code so the doubled-word check, +which a name like "Walla Walla County" trips, can skip this file alone. +""" + +#: Five-digit FIPS code -> county name, as the Water Data ``counties`` +#: collection spells it (snapshot 2026-10-01, ``country_code=US``, without its +#: ``000`` "Unspecified" placeholders and the minor outlying islands that +#: :mod:`~dataretrieval.codes.states` does not cover). +counties: dict[str, str] = { + "01001": "Autauga County", + "01003": "Baldwin County", + "01005": "Barbour County", + "01007": "Bibb County", + "01009": "Blount County", + "01011": "Bullock County", + "01013": "Butler County", + "01015": "Calhoun County", + "01017": "Chambers County", + "01019": "Cherokee County", + "01021": "Chilton County", + "01023": "Choctaw County", + "01025": "Clarke County", + "01027": "Clay County", + "01029": "Cleburne County", + "01031": "Coffee County", + "01033": "Colbert County", + "01035": "Conecuh County", + "01037": "Coosa County", + "01039": "Covington County", + "01041": "Crenshaw County", + "01043": "Cullman County", + "01045": "Dale County", + "01047": "Dallas County", + "01049": "DeKalb County", + "01051": "Elmore County", + "01053": "Escambia County", + "01055": "Etowah County", + "01057": "Fayette County", + "01059": "Franklin County", + "01061": "Geneva County", + "01063": "Greene County", + "01065": "Hale County", + "01067": "Henry County", + "01069": "Houston County", + "01071": "Jackson County", + "01073": "Jefferson County", + "01075": "Lamar County", + "01077": "Lauderdale County", + "01079": "Lawrence County", + "01081": "Lee County", + "01083": "Limestone County", + "01085": "Lowndes County", + "01087": "Macon County", + "01089": "Madison County", + "01091": "Marengo County", + "01093": "Marion County", + "01095": "Marshall County", + "01097": "Mobile County", + "01099": "Monroe County", + "01101": "Montgomery County", + "01103": "Morgan County", + "01105": "Perry County", + "01107": "Pickens County", + "01109": "Pike County", + "01111": "Randolph County", + "01113": "Russell County", + "01115": "St. Clair County", + "01117": "Shelby County", + "01119": "Sumter County", + "01121": "Talladega County", + "01123": "Tallapoosa County", + "01125": "Tuscaloosa County", + "01127": "Walker County", + "01129": "Washington County", + "01131": "Wilcox County", + "01133": "Winston County", + "02013": "Aleutians East Borough", + "02016": "Aleutians West Census Area", + "02020": "Anchorage Municipality", + "02050": "Bethel Census Area", + "02060": "Bristol Bay Borough", + "02063": "Chugach Census Area", + "02066": "Copper River Census Area", + "02068": "Denali Borough", + "02070": "Dillingham Census Area", + "02090": "Fairbanks North Star Borough", + "02100": "Haines Borough", + "02105": "Hoonah-Angoon Census Area", + "02110": "Juneau City and Borough", + "02122": "Kenai Peninsula Borough", + "02130": "Ketchikan Gateway Borough", + "02150": "Kodiak Island Borough", + "02158": "Kusilvak Census Area", + "02164": "Lake and Peninsula Borough", + "02170": "Matanuska-Susitna Borough", + "02180": "Nome Census Area", + "02185": "North Slope Borough", + "02188": "Northwest Arctic Borough", + "02195": "Petersburg Borough", + "02198": "Prince of Wales-Hyder Census Area", + "02220": "City and Borough of Sitka", + "02230": "Skagway Municipality", + "02240": "Southeast Fairbanks Census Area", + "02275": "Wrangell City and Borough", + "02282": "Yakutat City and Borough", + "02290": "Yukon-Koyukuk Census Area", + "04001": "Apache County", + "04003": "Cochise County", + "04005": "Coconino County", + "04007": "Gila County", + "04009": "Graham County", + "04011": "Greenlee County", + "04012": "La Paz County", + "04013": "Maricopa County", + "04015": "Mohave County", + "04017": "Navajo County", + "04019": "Pima County", + "04021": "Pinal County", + "04023": "Santa Cruz County", + "04025": "Yavapai County", + "04027": "Yuma County", + "05001": "Arkansas County", + "05003": "Ashley County", + "05005": "Baxter County", + "05007": "Benton County", + "05009": "Boone County", + "05011": "Bradley County", + "05013": "Calhoun County", + "05015": "Carroll County", + "05017": "Chicot County", + "05019": "Clark County", + "05021": "Clay County", + "05023": "Cleburne County", + "05025": "Cleveland County", + "05027": "Columbia County", + "05029": "Conway County", + "05031": "Craighead County", + "05033": "Crawford County", + "05035": "Crittenden County", + "05037": "Cross County", + "05039": "Dallas County", + "05041": "Desha County", + "05043": "Drew County", + "05045": "Faulkner County", + "05047": "Franklin County", + "05049": "Fulton County", + "05051": "Garland County", + "05053": "Grant County", + "05055": "Greene County", + "05057": "Hempstead County", + "05059": "Hot Spring County", + "05061": "Howard County", + "05063": "Independence County", + "05065": "Izard County", + "05067": "Jackson County", + "05069": "Jefferson County", + "05071": "Johnson County", + "05073": "Lafayette County", + "05075": "Lawrence County", + "05077": "Lee County", + "05079": "Lincoln County", + "05081": "Little River County", + "05083": "Logan County", + "05085": "Lonoke County", + "05087": "Madison County", + "05089": "Marion County", + "05091": "Miller County", + "05093": "Mississippi County", + "05095": "Monroe County", + "05097": "Montgomery County", + "05099": "Nevada County", + "05101": "Newton County", + "05103": "Ouachita County", + "05105": "Perry County", + "05107": "Phillips County", + "05109": "Pike County", + "05111": "Poinsett County", + "05113": "Polk County", + "05115": "Pope County", + "05117": "Prairie County", + "05119": "Pulaski County", + "05121": "Randolph County", + "05123": "St. Francis County", + "05125": "Saline County", + "05127": "Scott County", + "05129": "Searcy County", + "05131": "Sebastian County", + "05133": "Sevier County", + "05135": "Sharp County", + "05137": "Stone County", + "05139": "Union County", + "05141": "Van Buren County", + "05143": "Washington County", + "05145": "White County", + "05147": "Woodruff County", + "05149": "Yell County", + "06001": "Alameda County", + "06003": "Alpine County", + "06005": "Amador County", + "06007": "Butte County", + "06009": "Calaveras County", + "06011": "Colusa County", + "06013": "Contra Costa County", + "06015": "Del Norte County", + "06017": "El Dorado County", + "06019": "Fresno County", + "06021": "Glenn County", + "06023": "Humboldt County", + "06025": "Imperial County", + "06027": "Inyo County", + "06029": "Kern County", + "06031": "Kings County", + "06033": "Lake County", + "06035": "Lassen County", + "06037": "Los Angeles County", + "06039": "Madera County", + "06041": "Marin County", + "06043": "Mariposa County", + "06045": "Mendocino County", + "06047": "Merced County", + "06049": "Modoc County", + "06051": "Mono County", + "06053": "Monterey County", + "06055": "Napa County", + "06057": "Nevada County", + "06059": "Orange County", + "06061": "Placer County", + "06063": "Plumas County", + "06065": "Riverside County", + "06067": "Sacramento County", + "06069": "San Benito County", + "06071": "San Bernardino County", + "06073": "San Diego County", + "06075": "San Francisco County", + "06077": "San Joaquin County", + "06079": "San Luis Obispo County", + "06081": "San Mateo County", + "06083": "Santa Barbara County", + "06085": "Santa Clara County", + "06087": "Santa Cruz County", + "06089": "Shasta County", + "06091": "Sierra County", + "06093": "Siskiyou County", + "06095": "Solano County", + "06097": "Sonoma County", + "06099": "Stanislaus County", + "06101": "Sutter County", + "06103": "Tehama County", + "06105": "Trinity County", + "06107": "Tulare County", + "06109": "Tuolumne County", + "06111": "Ventura County", + "06113": "Yolo County", + "06115": "Yuba County", + "08001": "Adams County", + "08003": "Alamosa County", + "08005": "Arapahoe County", + "08007": "Archuleta County", + "08009": "Baca County", + "08011": "Bent County", + "08013": "Boulder County", + "08014": "Broomfield County", + "08015": "Chaffee County", + "08017": "Cheyenne County", + "08019": "Clear Creek County", + "08021": "Conejos County", + "08023": "Costilla County", + "08025": "Crowley County", + "08027": "Custer County", + "08029": "Delta County", + "08031": "Denver County", + "08033": "Dolores County", + "08035": "Douglas County", + "08037": "Eagle County", + "08039": "Elbert County", + "08041": "El Paso County", + "08043": "Fremont County", + "08045": "Garfield County", + "08047": "Gilpin County", + "08049": "Grand County", + "08051": "Gunnison County", + "08053": "Hinsdale County", + "08055": "Huerfano County", + "08057": "Jackson County", + "08059": "Jefferson County", + "08061": "Kiowa County", + "08063": "Kit Carson County", + "08065": "Lake County", + "08067": "La Plata County", + "08069": "Larimer County", + "08071": "Las Animas County", + "08073": "Lincoln County", + "08075": "Logan County", + "08077": "Mesa County", + "08079": "Mineral County", + "08081": "Moffat County", + "08083": "Montezuma County", + "08085": "Montrose County", + "08087": "Morgan County", + "08089": "Otero County", + "08091": "Ouray County", + "08093": "Park County", + "08095": "Phillips County", + "08097": "Pitkin County", + "08099": "Prowers County", + "08101": "Pueblo County", + "08103": "Rio Blanco County", + "08105": "Rio Grande County", + "08107": "Routt County", + "08109": "Saguache County", + "08111": "San Juan County", + "08113": "San Miguel County", + "08115": "Sedgwick County", + "08117": "Summit County", + "08119": "Teller County", + "08121": "Washington County", + "08123": "Weld County", + "08125": "Yuma County", + "09110": "Capitol Planning Region", + "09120": "Greater Bridgeport Planning Region", + "09130": "Lower Connecticut River Valley Planning Region", + "09140": "Naugatuck Valley Planning Region", + "09150": "Northeastern Connecticut Planning Region", + "09160": "Northwest Hills Planning Region", + "09170": "South Central Connecticut Planning Region", + "09180": "Southeastern Connecticut Planning Region", + "09190": "Western Connecticut Planning Region", + "10001": "Kent County", + "10003": "New Castle County", + "10005": "Sussex County", + "11001": "District of Columbia", + "12001": "Alachua County", + "12003": "Baker County", + "12005": "Bay County", + "12007": "Bradford County", + "12009": "Brevard County", + "12011": "Broward County", + "12013": "Calhoun County", + "12015": "Charlotte County", + "12017": "Citrus County", + "12019": "Clay County", + "12021": "Collier County", + "12023": "Columbia County", + "12027": "DeSoto County", + "12029": "Dixie County", + "12031": "Duval County", + "12033": "Escambia County", + "12035": "Flagler County", + "12037": "Franklin County", + "12039": "Gadsden County", + "12041": "Gilchrist County", + "12043": "Glades County", + "12045": "Gulf County", + "12047": "Hamilton County", + "12049": "Hardee County", + "12051": "Hendry County", + "12053": "Hernando County", + "12055": "Highlands County", + "12057": "Hillsborough County", + "12059": "Holmes County", + "12061": "Indian River County", + "12063": "Jackson County", + "12065": "Jefferson County", + "12067": "Lafayette County", + "12069": "Lake County", + "12071": "Lee County", + "12073": "Leon County", + "12075": "Levy County", + "12077": "Liberty County", + "12079": "Madison County", + "12081": "Manatee County", + "12083": "Marion County", + "12085": "Martin County", + "12086": "Miami-Dade County", + "12087": "Monroe County", + "12089": "Nassau County", + "12091": "Okaloosa County", + "12093": "Okeechobee County", + "12095": "Orange County", + "12097": "Osceola County", + "12099": "Palm Beach County", + "12101": "Pasco County", + "12103": "Pinellas County", + "12105": "Polk County", + "12107": "Putnam County", + "12109": "St. Johns County", + "12111": "St. Lucie County", + "12113": "Santa Rosa County", + "12115": "Sarasota County", + "12117": "Seminole County", + "12119": "Sumter County", + "12121": "Suwannee County", + "12123": "Taylor County", + "12125": "Union County", + "12127": "Volusia County", + "12129": "Wakulla County", + "12131": "Walton County", + "12133": "Washington County", + "13001": "Appling County", + "13003": "Atkinson County", + "13005": "Bacon County", + "13007": "Baker County", + "13009": "Baldwin County", + "13011": "Banks County", + "13013": "Barrow County", + "13015": "Bartow County", + "13017": "Ben Hill County", + "13019": "Berrien County", + "13021": "Bibb County", + "13023": "Bleckley County", + "13025": "Brantley County", + "13027": "Brooks County", + "13029": "Bryan County", + "13031": "Bulloch County", + "13033": "Burke County", + "13035": "Butts County", + "13037": "Calhoun County", + "13039": "Camden County", + "13043": "Candler County", + "13045": "Carroll County", + "13047": "Catoosa County", + "13049": "Charlton County", + "13051": "Chatham County", + "13053": "Chattahoochee County", + "13055": "Chattooga County", + "13057": "Cherokee County", + "13059": "Clarke County", + "13061": "Clay County", + "13063": "Clayton County", + "13065": "Clinch County", + "13067": "Cobb County", + "13069": "Coffee County", + "13071": "Colquitt County", + "13073": "Columbia County", + "13075": "Cook County", + "13077": "Coweta County", + "13079": "Crawford County", + "13081": "Crisp County", + "13083": "Dade County", + "13085": "Dawson County", + "13087": "Decatur County", + "13089": "DeKalb County", + "13091": "Dodge County", + "13093": "Dooly County", + "13095": "Dougherty County", + "13097": "Douglas County", + "13099": "Early County", + "13101": "Echols County", + "13103": "Effingham County", + "13105": "Elbert County", + "13107": "Emanuel County", + "13109": "Evans County", + "13111": "Fannin County", + "13113": "Fayette County", + "13115": "Floyd County", + "13117": "Forsyth County", + "13119": "Franklin County", + "13121": "Fulton County", + "13123": "Gilmer County", + "13125": "Glascock County", + "13127": "Glynn County", + "13129": "Gordon County", + "13131": "Grady County", + "13133": "Greene County", + "13135": "Gwinnett County", + "13137": "Habersham County", + "13139": "Hall County", + "13141": "Hancock County", + "13143": "Haralson County", + "13145": "Harris County", + "13147": "Hart County", + "13149": "Heard County", + "13151": "Henry County", + "13153": "Houston County", + "13155": "Irwin County", + "13157": "Jackson County", + "13159": "Jasper County", + "13161": "Jeff Davis County", + "13163": "Jefferson County", + "13165": "Jenkins County", + "13167": "Johnson County", + "13169": "Jones County", + "13171": "Lamar County", + "13173": "Lanier County", + "13175": "Laurens County", + "13177": "Lee County", + "13179": "Liberty County", + "13181": "Lincoln County", + "13183": "Long County", + "13185": "Lowndes County", + "13187": "Lumpkin County", + "13189": "McDuffie County", + "13191": "McIntosh County", + "13193": "Macon County", + "13195": "Madison County", + "13197": "Marion County", + "13199": "Meriwether County", + "13201": "Miller County", + "13205": "Mitchell County", + "13207": "Monroe County", + "13209": "Montgomery County", + "13211": "Morgan County", + "13213": "Murray County", + "13215": "Muscogee County", + "13217": "Newton County", + "13219": "Oconee County", + "13221": "Oglethorpe County", + "13223": "Paulding County", + "13225": "Peach County", + "13227": "Pickens County", + "13229": "Pierce County", + "13231": "Pike County", + "13233": "Polk County", + "13235": "Pulaski County", + "13237": "Putnam County", + "13239": "Quitman County", + "13241": "Rabun County", + "13243": "Randolph County", + "13245": "Richmond County", + "13247": "Rockdale County", + "13249": "Schley County", + "13251": "Screven County", + "13253": "Seminole County", + "13255": "Spalding County", + "13257": "Stephens County", + "13259": "Stewart County", + "13261": "Sumter County", + "13263": "Talbot County", + "13265": "Taliaferro County", + "13267": "Tattnall County", + "13269": "Taylor County", + "13271": "Telfair County", + "13273": "Terrell County", + "13275": "Thomas County", + "13277": "Tift County", + "13279": "Toombs County", + "13281": "Towns County", + "13283": "Treutlen County", + "13285": "Troup County", + "13287": "Turner County", + "13289": "Twiggs County", + "13291": "Union County", + "13293": "Upson County", + "13295": "Walker County", + "13297": "Walton County", + "13299": "Ware County", + "13301": "Warren County", + "13303": "Washington County", + "13305": "Wayne County", + "13307": "Webster County", + "13309": "Wheeler County", + "13311": "White County", + "13313": "Whitfield County", + "13315": "Wilcox County", + "13317": "Wilkes County", + "13319": "Wilkinson County", + "13321": "Worth County", + "15001": "Hawaii County", + "15003": "Honolulu County", + "15005": "Kalawao County", + "15007": "Kauai County", + "15009": "Maui County", + "16001": "Ada County", + "16003": "Adams County", + "16005": "Bannock County", + "16007": "Bear Lake County", + "16009": "Benewah County", + "16011": "Bingham County", + "16013": "Blaine County", + "16015": "Boise County", + "16017": "Bonner County", + "16019": "Bonneville County", + "16021": "Boundary County", + "16023": "Butte County", + "16025": "Camas County", + "16027": "Canyon County", + "16029": "Caribou County", + "16031": "Cassia County", + "16033": "Clark County", + "16035": "Clearwater County", + "16037": "Custer County", + "16039": "Elmore County", + "16041": "Franklin County", + "16043": "Fremont County", + "16045": "Gem County", + "16047": "Gooding County", + "16049": "Idaho County", + "16051": "Jefferson County", + "16053": "Jerome County", + "16055": "Kootenai County", + "16057": "Latah County", + "16059": "Lemhi County", + "16061": "Lewis County", + "16063": "Lincoln County", + "16065": "Madison County", + "16067": "Minidoka County", + "16069": "Nez Perce County", + "16071": "Oneida County", + "16073": "Owyhee County", + "16075": "Payette County", + "16077": "Power County", + "16079": "Shoshone County", + "16081": "Teton County", + "16083": "Twin Falls County", + "16085": "Valley County", + "16087": "Washington County", + "17001": "Adams County", + "17003": "Alexander County", + "17005": "Bond County", + "17007": "Boone County", + "17009": "Brown County", + "17011": "Bureau County", + "17013": "Calhoun County", + "17015": "Carroll County", + "17017": "Cass County", + "17019": "Champaign County", + "17021": "Christian County", + "17023": "Clark County", + "17025": "Clay County", + "17027": "Clinton County", + "17029": "Coles County", + "17031": "Cook County", + "17033": "Crawford County", + "17035": "Cumberland County", + "17037": "DeKalb County", + "17039": "DeWitt County", + "17041": "Douglas County", + "17043": "DuPage County", + "17045": "Edgar County", + "17047": "Edwards County", + "17049": "Effingham County", + "17051": "Fayette County", + "17053": "Ford County", + "17055": "Franklin County", + "17057": "Fulton County", + "17059": "Gallatin County", + "17061": "Greene County", + "17063": "Grundy County", + "17065": "Hamilton County", + "17067": "Hancock County", + "17069": "Hardin County", + "17071": "Henderson County", + "17073": "Henry County", + "17075": "Iroquois County", + "17077": "Jackson County", + "17079": "Jasper County", + "17081": "Jefferson County", + "17083": "Jersey County", + "17085": "Jo Daviess County", + "17087": "Johnson County", + "17089": "Kane County", + "17091": "Kankakee County", + "17093": "Kendall County", + "17095": "Knox County", + "17097": "Lake County", + "17099": "LaSalle County", + "17101": "Lawrence County", + "17103": "Lee County", + "17105": "Livingston County", + "17107": "Logan County", + "17109": "McDonough County", + "17111": "McHenry County", + "17113": "McLean County", + "17115": "Macon County", + "17117": "Macoupin County", + "17119": "Madison County", + "17121": "Marion County", + "17123": "Marshall County", + "17125": "Mason County", + "17127": "Massac County", + "17129": "Menard County", + "17131": "Mercer County", + "17133": "Monroe County", + "17135": "Montgomery County", + "17137": "Morgan County", + "17139": "Moultrie County", + "17141": "Ogle County", + "17143": "Peoria County", + "17145": "Perry County", + "17147": "Piatt County", + "17149": "Pike County", + "17151": "Pope County", + "17153": "Pulaski County", + "17155": "Putnam County", + "17157": "Randolph County", + "17159": "Richland County", + "17161": "Rock Island County", + "17163": "St. Clair County", + "17165": "Saline County", + "17167": "Sangamon County", + "17169": "Schuyler County", + "17171": "Scott County", + "17173": "Shelby County", + "17175": "Stark County", + "17177": "Stephenson County", + "17179": "Tazewell County", + "17181": "Union County", + "17183": "Vermilion County", + "17185": "Wabash County", + "17187": "Warren County", + "17189": "Washington County", + "17191": "Wayne County", + "17193": "White County", + "17195": "Whiteside County", + "17197": "Will County", + "17199": "Williamson County", + "17201": "Winnebago County", + "17203": "Woodford County", + "18001": "Adams County", + "18003": "Allen County", + "18005": "Bartholomew County", + "18007": "Benton County", + "18009": "Blackford County", + "18011": "Boone County", + "18013": "Brown County", + "18015": "Carroll County", + "18017": "Cass County", + "18019": "Clark County", + "18021": "Clay County", + "18023": "Clinton County", + "18025": "Crawford County", + "18027": "Daviess County", + "18029": "Dearborn County", + "18031": "Decatur County", + "18033": "DeKalb County", + "18035": "Delaware County", + "18037": "Dubois County", + "18039": "Elkhart County", + "18041": "Fayette County", + "18043": "Floyd County", + "18045": "Fountain County", + "18047": "Franklin County", + "18049": "Fulton County", + "18051": "Gibson County", + "18053": "Grant County", + "18055": "Greene County", + "18057": "Hamilton County", + "18059": "Hancock County", + "18061": "Harrison County", + "18063": "Hendricks County", + "18065": "Henry County", + "18067": "Howard County", + "18069": "Huntington County", + "18071": "Jackson County", + "18073": "Jasper County", + "18075": "Jay County", + "18077": "Jefferson County", + "18079": "Jennings County", + "18081": "Johnson County", + "18083": "Knox County", + "18085": "Kosciusko County", + "18087": "LaGrange County", + "18089": "Lake County", + "18091": "LaPorte County", + "18093": "Lawrence County", + "18095": "Madison County", + "18097": "Marion County", + "18099": "Marshall County", + "18101": "Martin County", + "18103": "Miami County", + "18105": "Monroe County", + "18107": "Montgomery County", + "18109": "Morgan County", + "18111": "Newton County", + "18113": "Noble County", + "18115": "Ohio County", + "18117": "Orange County", + "18119": "Owen County", + "18121": "Parke County", + "18123": "Perry County", + "18125": "Pike County", + "18127": "Porter County", + "18129": "Posey County", + "18131": "Pulaski County", + "18133": "Putnam County", + "18135": "Randolph County", + "18137": "Ripley County", + "18139": "Rush County", + "18141": "St. Joseph County", + "18143": "Scott County", + "18145": "Shelby County", + "18147": "Spencer County", + "18149": "Starke County", + "18151": "Steuben County", + "18153": "Sullivan County", + "18155": "Switzerland County", + "18157": "Tippecanoe County", + "18159": "Tipton County", + "18161": "Union County", + "18163": "Vanderburgh County", + "18165": "Vermillion County", + "18167": "Vigo County", + "18169": "Wabash County", + "18171": "Warren County", + "18173": "Warrick County", + "18175": "Washington County", + "18177": "Wayne County", + "18179": "Wells County", + "18181": "White County", + "18183": "Whitley County", + "19001": "Adair County", + "19003": "Adams County", + "19005": "Allamakee County", + "19007": "Appanoose County", + "19009": "Audubon County", + "19011": "Benton County", + "19013": "Black Hawk County", + "19015": "Boone County", + "19017": "Bremer County", + "19019": "Buchanan County", + "19021": "Buena Vista County", + "19023": "Butler County", + "19025": "Calhoun County", + "19027": "Carroll County", + "19029": "Cass County", + "19031": "Cedar County", + "19033": "Cerro Gordo County", + "19035": "Cherokee County", + "19037": "Chickasaw County", + "19039": "Clarke County", + "19041": "Clay County", + "19043": "Clayton County", + "19045": "Clinton County", + "19047": "Crawford County", + "19049": "Dallas County", + "19051": "Davis County", + "19053": "Decatur County", + "19055": "Delaware County", + "19057": "Des Moines County", + "19059": "Dickinson County", + "19061": "Dubuque County", + "19063": "Emmet County", + "19065": "Fayette County", + "19067": "Floyd County", + "19069": "Franklin County", + "19071": "Fremont County", + "19073": "Greene County", + "19075": "Grundy County", + "19077": "Guthrie County", + "19079": "Hamilton County", + "19081": "Hancock County", + "19083": "Hardin County", + "19085": "Harrison County", + "19087": "Henry County", + "19089": "Howard County", + "19091": "Humboldt County", + "19093": "Ida County", + "19095": "Iowa County", + "19097": "Jackson County", + "19099": "Jasper County", + "19101": "Jefferson County", + "19103": "Johnson County", + "19105": "Jones County", + "19107": "Keokuk County", + "19109": "Kossuth County", + "19111": "Lee County", + "19113": "Linn County", + "19115": "Louisa County", + "19117": "Lucas County", + "19119": "Lyon County", + "19121": "Madison County", + "19123": "Mahaska County", + "19125": "Marion County", + "19127": "Marshall County", + "19129": "Mills County", + "19131": "Mitchell County", + "19133": "Monona County", + "19135": "Monroe County", + "19137": "Montgomery County", + "19139": "Muscatine County", + "19141": "O'Brien County", + "19143": "Osceola County", + "19145": "Page County", + "19147": "Palo Alto County", + "19149": "Plymouth County", + "19151": "Pocahontas County", + "19153": "Polk County", + "19155": "Pottawattamie County", + "19157": "Poweshiek County", + "19159": "Ringgold County", + "19161": "Sac County", + "19163": "Scott County", + "19165": "Shelby County", + "19167": "Sioux County", + "19169": "Story County", + "19171": "Tama County", + "19173": "Taylor County", + "19175": "Union County", + "19177": "Van Buren County", + "19179": "Wapello County", + "19181": "Warren County", + "19183": "Washington County", + "19185": "Wayne County", + "19187": "Webster County", + "19189": "Winnebago County", + "19191": "Winneshiek County", + "19193": "Woodbury County", + "19195": "Worth County", + "19197": "Wright County", + "20001": "Allen County", + "20003": "Anderson County", + "20005": "Atchison County", + "20007": "Barber County", + "20009": "Barton County", + "20011": "Bourbon County", + "20013": "Brown County", + "20015": "Butler County", + "20017": "Chase County", + "20019": "Chautauqua County", + "20021": "Cherokee County", + "20023": "Cheyenne County", + "20025": "Clark County", + "20027": "Clay County", + "20029": "Cloud County", + "20031": "Coffey County", + "20033": "Comanche County", + "20035": "Cowley County", + "20037": "Crawford County", + "20039": "Decatur County", + "20041": "Dickinson County", + "20043": "Doniphan County", + "20045": "Douglas County", + "20047": "Edwards County", + "20049": "Elk County", + "20051": "Ellis County", + "20053": "Ellsworth County", + "20055": "Finney County", + "20057": "Ford County", + "20059": "Franklin County", + "20061": "Geary County", + "20063": "Gove County", + "20065": "Graham County", + "20067": "Grant County", + "20069": "Gray County", + "20071": "Greeley County", + "20073": "Greenwood County", + "20075": "Hamilton County", + "20077": "Harper County", + "20079": "Harvey County", + "20081": "Haskell County", + "20083": "Hodgeman County", + "20085": "Jackson County", + "20087": "Jefferson County", + "20089": "Jewell County", + "20091": "Johnson County", + "20093": "Kearny County", + "20095": "Kingman County", + "20097": "Kiowa County", + "20099": "Labette County", + "20101": "Lane County", + "20103": "Leavenworth County", + "20105": "Lincoln County", + "20107": "Linn County", + "20109": "Logan County", + "20111": "Lyon County", + "20113": "McPherson County", + "20115": "Marion County", + "20117": "Marshall County", + "20119": "Meade County", + "20121": "Miami County", + "20123": "Mitchell County", + "20125": "Montgomery County", + "20127": "Morris County", + "20129": "Morton County", + "20131": "Nemaha County", + "20133": "Neosho County", + "20135": "Ness County", + "20137": "Norton County", + "20139": "Osage County", + "20141": "Osborne County", + "20143": "Ottawa County", + "20145": "Pawnee County", + "20147": "Phillips County", + "20149": "Pottawatomie County", + "20151": "Pratt County", + "20153": "Rawlins County", + "20155": "Reno County", + "20157": "Republic County", + "20159": "Rice County", + "20161": "Riley County", + "20163": "Rooks County", + "20165": "Rush County", + "20167": "Russell County", + "20169": "Saline County", + "20171": "Scott County", + "20173": "Sedgwick County", + "20175": "Seward County", + "20177": "Shawnee County", + "20179": "Sheridan County", + "20181": "Sherman County", + "20183": "Smith County", + "20185": "Stafford County", + "20187": "Stanton County", + "20189": "Stevens County", + "20191": "Sumner County", + "20193": "Thomas County", + "20195": "Trego County", + "20197": "Wabaunsee County", + "20199": "Wallace County", + "20201": "Washington County", + "20203": "Wichita County", + "20205": "Wilson County", + "20207": "Woodson County", + "20209": "Wyandotte County", + "21001": "Adair County", + "21003": "Allen County", + "21005": "Anderson County", + "21007": "Ballard County", + "21009": "Barren County", + "21011": "Bath County", + "21013": "Bell County", + "21015": "Boone County", + "21017": "Bourbon County", + "21019": "Boyd County", + "21021": "Boyle County", + "21023": "Bracken County", + "21025": "Breathitt County", + "21027": "Breckinridge County", + "21029": "Bullitt County", + "21031": "Butler County", + "21033": "Caldwell County", + "21035": "Calloway County", + "21037": "Campbell County", + "21039": "Carlisle County", + "21041": "Carroll County", + "21043": "Carter County", + "21045": "Casey County", + "21047": "Christian County", + "21049": "Clark County", + "21051": "Clay County", + "21053": "Clinton County", + "21055": "Crittenden County", + "21057": "Cumberland County", + "21059": "Daviess County", + "21061": "Edmonson County", + "21063": "Elliott County", + "21065": "Estill County", + "21067": "Fayette County", + "21069": "Fleming County", + "21071": "Floyd County", + "21073": "Franklin County", + "21075": "Fulton County", + "21077": "Gallatin County", + "21079": "Garrard County", + "21081": "Grant County", + "21083": "Graves County", + "21085": "Grayson County", + "21087": "Green County", + "21089": "Greenup County", + "21091": "Hancock County", + "21093": "Hardin County", + "21095": "Harlan County", + "21097": "Harrison County", + "21099": "Hart County", + "21101": "Henderson County", + "21103": "Henry County", + "21105": "Hickman County", + "21107": "Hopkins County", + "21109": "Jackson County", + "21111": "Jefferson County", + "21113": "Jessamine County", + "21115": "Johnson County", + "21117": "Kenton County", + "21119": "Knott County", + "21121": "Knox County", + "21123": "Larue County", + "21125": "Laurel County", + "21127": "Lawrence County", + "21129": "Lee County", + "21131": "Leslie County", + "21133": "Letcher County", + "21135": "Lewis County", + "21137": "Lincoln County", + "21139": "Livingston County", + "21141": "Logan County", + "21143": "Lyon County", + "21145": "McCracken County", + "21147": "McCreary County", + "21149": "McLean County", + "21151": "Madison County", + "21153": "Magoffin County", + "21155": "Marion County", + "21157": "Marshall County", + "21159": "Martin County", + "21161": "Mason County", + "21163": "Meade County", + "21165": "Menifee County", + "21167": "Mercer County", + "21169": "Metcalfe County", + "21171": "Monroe County", + "21173": "Montgomery County", + "21175": "Morgan County", + "21177": "Muhlenberg County", + "21179": "Nelson County", + "21181": "Nicholas County", + "21183": "Ohio County", + "21185": "Oldham County", + "21187": "Owen County", + "21189": "Owsley County", + "21191": "Pendleton County", + "21193": "Perry County", + "21195": "Pike County", + "21197": "Powell County", + "21199": "Pulaski County", + "21201": "Robertson County", + "21203": "Rockcastle County", + "21205": "Rowan County", + "21207": "Russell County", + "21209": "Scott County", + "21211": "Shelby County", + "21213": "Simpson County", + "21215": "Spencer County", + "21217": "Taylor County", + "21219": "Todd County", + "21221": "Trigg County", + "21223": "Trimble County", + "21225": "Union County", + "21227": "Warren County", + "21229": "Washington County", + "21231": "Wayne County", + "21233": "Webster County", + "21235": "Whitley County", + "21237": "Wolfe County", + "21239": "Woodford County", + "22001": "Acadia Parish", + "22003": "Allen Parish", + "22005": "Ascension Parish", + "22007": "Assumption Parish", + "22009": "Avoyelles Parish", + "22011": "Beauregard Parish", + "22013": "Bienville Parish", + "22015": "Bossier Parish", + "22017": "Caddo Parish", + "22019": "Calcasieu Parish", + "22021": "Caldwell Parish", + "22023": "Cameron Parish", + "22025": "Catahoula Parish", + "22027": "Claiborne Parish", + "22029": "Concordia Parish", + "22031": "De Soto Parish", + "22033": "East Baton Rouge Parish", + "22035": "East Carroll Parish", + "22037": "East Feliciana Parish", + "22039": "Evangeline Parish", + "22041": "Franklin Parish", + "22043": "Grant Parish", + "22045": "Iberia Parish", + "22047": "Iberville Parish", + "22049": "Jackson Parish", + "22051": "Jefferson Parish", + "22053": "Jefferson Davis Parish", + "22055": "Lafayette Parish", + "22057": "Lafourche Parish", + "22059": "LaSalle Parish", + "22061": "Lincoln Parish", + "22063": "Livingston Parish", + "22065": "Madison Parish", + "22067": "Morehouse Parish", + "22069": "Natchitoches Parish", + "22071": "Orleans Parish", + "22073": "Ouachita Parish", + "22075": "Plaquemines Parish", + "22077": "Pointe Coupee Parish", + "22079": "Rapides Parish", + "22081": "Red River Parish", + "22083": "Richland Parish", + "22085": "Sabine Parish", + "22087": "St. Bernard Parish", + "22089": "St. Charles Parish", + "22091": "St. Helena Parish", + "22093": "St. James Parish", + "22095": "St. John the Baptist Parish", + "22097": "St. Landry Parish", + "22099": "St. Martin Parish", + "22101": "St. Mary Parish", + "22103": "St. Tammany Parish", + "22105": "Tangipahoa Parish", + "22107": "Tensas Parish", + "22109": "Terrebonne Parish", + "22111": "Union Parish", + "22113": "Vermilion Parish", + "22115": "Vernon Parish", + "22117": "Washington Parish", + "22119": "Webster Parish", + "22121": "West Baton Rouge Parish", + "22123": "West Carroll Parish", + "22125": "West Feliciana Parish", + "22127": "Winn Parish", + "23001": "Androscoggin County", + "23003": "Aroostook County", + "23005": "Cumberland County", + "23007": "Franklin County", + "23009": "Hancock County", + "23011": "Kennebec County", + "23013": "Knox County", + "23015": "Lincoln County", + "23017": "Oxford County", + "23019": "Penobscot County", + "23021": "Piscataquis County", + "23023": "Sagadahoc County", + "23025": "Somerset County", + "23027": "Waldo County", + "23029": "Washington County", + "23031": "York County", + "24001": "Allegany County", + "24003": "Anne Arundel County", + "24005": "Baltimore County", + "24009": "Calvert County", + "24011": "Caroline County", + "24013": "Carroll County", + "24015": "Cecil County", + "24017": "Charles County", + "24019": "Dorchester County", + "24021": "Frederick County", + "24023": "Garrett County", + "24025": "Harford County", + "24027": "Howard County", + "24029": "Kent County", + "24031": "Montgomery County", + "24033": "Prince George's County", + "24035": "Queen Anne's County", + "24037": "St. Mary's County", + "24039": "Somerset County", + "24041": "Talbot County", + "24043": "Washington County", + "24045": "Wicomico County", + "24047": "Worcester County", + "24510": "Baltimore City", + "25001": "Barnstable County", + "25003": "Berkshire County", + "25005": "Bristol County", + "25007": "Dukes County", + "25009": "Essex County", + "25011": "Franklin County", + "25013": "Hampden County", + "25015": "Hampshire County", + "25017": "Middlesex County", + "25019": "Nantucket County", + "25021": "Norfolk County", + "25023": "Plymouth County", + "25025": "Suffolk County", + "25027": "Worcester County", + "26001": "Alcona County", + "26003": "Alger County", + "26005": "Allegan County", + "26007": "Alpena County", + "26009": "Antrim County", + "26011": "Arenac County", + "26013": "Baraga County", + "26015": "Barry County", + "26017": "Bay County", + "26019": "Benzie County", + "26021": "Berrien County", + "26023": "Branch County", + "26025": "Calhoun County", + "26027": "Cass County", + "26029": "Charlevoix County", + "26031": "Cheboygan County", + "26033": "Chippewa County", + "26035": "Clare County", + "26037": "Clinton County", + "26039": "Crawford County", + "26041": "Delta County", + "26043": "Dickinson County", + "26045": "Eaton County", + "26047": "Emmet County", + "26049": "Genesee County", + "26051": "Gladwin County", + "26053": "Gogebic County", + "26055": "Grand Traverse County", + "26057": "Gratiot County", + "26059": "Hillsdale County", + "26061": "Houghton County", + "26063": "Huron County", + "26065": "Ingham County", + "26067": "Ionia County", + "26069": "Iosco County", + "26071": "Iron County", + "26073": "Isabella County", + "26075": "Jackson County", + "26077": "Kalamazoo County", + "26079": "Kalkaska County", + "26081": "Kent County", + "26083": "Keweenaw County", + "26085": "Lake County", + "26087": "Lapeer County", + "26089": "Leelanau County", + "26091": "Lenawee County", + "26093": "Livingston County", + "26095": "Luce County", + "26097": "Mackinac County", + "26099": "Macomb County", + "26101": "Manistee County", + "26103": "Marquette County", + "26105": "Mason County", + "26107": "Mecosta County", + "26109": "Menominee County", + "26111": "Midland County", + "26113": "Missaukee County", + "26115": "Monroe County", + "26117": "Montcalm County", + "26119": "Montmorency County", + "26121": "Muskegon County", + "26123": "Newaygo County", + "26125": "Oakland County", + "26127": "Oceana County", + "26129": "Ogemaw County", + "26131": "Ontonagon County", + "26133": "Osceola County", + "26135": "Oscoda County", + "26137": "Otsego County", + "26139": "Ottawa County", + "26141": "Presque Isle County", + "26143": "Roscommon County", + "26145": "Saginaw County", + "26147": "St. Clair County", + "26149": "St. Joseph County", + "26151": "Sanilac County", + "26153": "Schoolcraft County", + "26155": "Shiawassee County", + "26157": "Tuscola County", + "26159": "Van Buren County", + "26161": "Washtenaw County", + "26163": "Wayne County", + "26165": "Wexford County", + "27001": "Aitkin County", + "27003": "Anoka County", + "27005": "Becker County", + "27007": "Beltrami County", + "27009": "Benton County", + "27011": "Big Stone County", + "27013": "Blue Earth County", + "27015": "Brown County", + "27017": "Carlton County", + "27019": "Carver County", + "27021": "Cass County", + "27023": "Chippewa County", + "27025": "Chisago County", + "27027": "Clay County", + "27029": "Clearwater County", + "27031": "Cook County", + "27033": "Cottonwood County", + "27035": "Crow Wing County", + "27037": "Dakota County", + "27039": "Dodge County", + "27041": "Douglas County", + "27043": "Faribault County", + "27045": "Fillmore County", + "27047": "Freeborn County", + "27049": "Goodhue County", + "27051": "Grant County", + "27053": "Hennepin County", + "27055": "Houston County", + "27057": "Hubbard County", + "27059": "Isanti County", + "27061": "Itasca County", + "27063": "Jackson County", + "27065": "Kanabec County", + "27067": "Kandiyohi County", + "27069": "Kittson County", + "27071": "Koochiching County", + "27073": "Lac qui Parle County", + "27075": "Lake County", + "27077": "Lake of the Woods County", + "27079": "Le Sueur County", + "27081": "Lincoln County", + "27083": "Lyon County", + "27085": "McLeod County", + "27087": "Mahnomen County", + "27089": "Marshall County", + "27091": "Martin County", + "27093": "Meeker County", + "27095": "Mille Lacs County", + "27097": "Morrison County", + "27099": "Mower County", + "27101": "Murray County", + "27103": "Nicollet County", + "27105": "Nobles County", + "27107": "Norman County", + "27109": "Olmsted County", + "27111": "Otter Tail County", + "27113": "Pennington County", + "27115": "Pine County", + "27117": "Pipestone County", + "27119": "Polk County", + "27121": "Pope County", + "27123": "Ramsey County", + "27125": "Red Lake County", + "27127": "Redwood County", + "27129": "Renville County", + "27131": "Rice County", + "27133": "Rock County", + "27135": "Roseau County", + "27137": "St. Louis County", + "27139": "Scott County", + "27141": "Sherburne County", + "27143": "Sibley County", + "27145": "Stearns County", + "27147": "Steele County", + "27149": "Stevens County", + "27151": "Swift County", + "27153": "Todd County", + "27155": "Traverse County", + "27157": "Wabasha County", + "27159": "Wadena County", + "27161": "Waseca County", + "27163": "Washington County", + "27165": "Watonwan County", + "27167": "Wilkin County", + "27169": "Winona County", + "27171": "Wright County", + "27173": "Yellow Medicine County", + "28001": "Adams County", + "28003": "Alcorn County", + "28005": "Amite County", + "28007": "Attala County", + "28009": "Benton County", + "28011": "Bolivar County", + "28013": "Calhoun County", + "28015": "Carroll County", + "28017": "Chickasaw County", + "28019": "Choctaw County", + "28021": "Claiborne County", + "28023": "Clarke County", + "28025": "Clay County", + "28027": "Coahoma County", + "28029": "Copiah County", + "28031": "Covington County", + "28033": "DeSoto County", + "28035": "Forrest County", + "28037": "Franklin County", + "28039": "George County", + "28041": "Greene County", + "28043": "Grenada County", + "28045": "Hancock County", + "28047": "Harrison County", + "28049": "Hinds County", + "28051": "Holmes County", + "28053": "Humphreys County", + "28055": "Issaquena County", + "28057": "Itawamba County", + "28059": "Jackson County", + "28061": "Jasper County", + "28063": "Jefferson County", + "28065": "Jefferson Davis County", + "28067": "Jones County", + "28069": "Kemper County", + "28071": "Lafayette County", + "28073": "Lamar County", + "28075": "Lauderdale County", + "28077": "Lawrence County", + "28079": "Leake County", + "28081": "Lee County", + "28083": "Leflore County", + "28085": "Lincoln County", + "28087": "Lowndes County", + "28089": "Madison County", + "28091": "Marion County", + "28093": "Marshall County", + "28095": "Monroe County", + "28097": "Montgomery County", + "28099": "Neshoba County", + "28101": "Newton County", + "28103": "Noxubee County", + "28105": "Oktibbeha County", + "28107": "Panola County", + "28109": "Pearl River County", + "28111": "Perry County", + "28113": "Pike County", + "28115": "Pontotoc County", + "28117": "Prentiss County", + "28119": "Quitman County", + "28121": "Rankin County", + "28123": "Scott County", + "28125": "Sharkey County", + "28127": "Simpson County", + "28129": "Smith County", + "28131": "Stone County", + "28133": "Sunflower County", + "28135": "Tallahatchie County", + "28137": "Tate County", + "28139": "Tippah County", + "28141": "Tishomingo County", + "28143": "Tunica County", + "28145": "Union County", + "28147": "Walthall County", + "28149": "Warren County", + "28151": "Washington County", + "28153": "Wayne County", + "28155": "Webster County", + "28157": "Wilkinson County", + "28159": "Winston County", + "28161": "Yalobusha County", + "28163": "Yazoo County", + "29001": "Adair County", + "29003": "Andrew County", + "29005": "Atchison County", + "29007": "Audrain County", + "29009": "Barry County", + "29011": "Barton County", + "29013": "Bates County", + "29015": "Benton County", + "29017": "Bollinger County", + "29019": "Boone County", + "29021": "Buchanan County", + "29023": "Butler County", + "29025": "Caldwell County", + "29027": "Callaway County", + "29029": "Camden County", + "29031": "Cape Girardeau County", + "29033": "Carroll County", + "29035": "Carter County", + "29037": "Cass County", + "29039": "Cedar County", + "29041": "Chariton County", + "29043": "Christian County", + "29045": "Clark County", + "29047": "Clay County", + "29049": "Clinton County", + "29051": "Cole County", + "29053": "Cooper County", + "29055": "Crawford County", + "29057": "Dade County", + "29059": "Dallas County", + "29061": "Daviess County", + "29063": "DeKalb County", + "29065": "Dent County", + "29067": "Douglas County", + "29069": "Dunklin County", + "29071": "Franklin County", + "29073": "Gasconade County", + "29075": "Gentry County", + "29077": "Greene County", + "29079": "Grundy County", + "29081": "Harrison County", + "29083": "Henry County", + "29085": "Hickory County", + "29087": "Holt County", + "29089": "Howard County", + "29091": "Howell County", + "29093": "Iron County", + "29095": "Jackson County", + "29097": "Jasper County", + "29099": "Jefferson County", + "29101": "Johnson County", + "29103": "Knox County", + "29105": "Laclede County", + "29107": "Lafayette County", + "29109": "Lawrence County", + "29111": "Lewis County", + "29113": "Lincoln County", + "29115": "Linn County", + "29117": "Livingston County", + "29119": "McDonald County", + "29121": "Macon County", + "29123": "Madison County", + "29125": "Maries County", + "29127": "Marion County", + "29129": "Mercer County", + "29131": "Miller County", + "29133": "Mississippi County", + "29135": "Moniteau County", + "29137": "Monroe County", + "29139": "Montgomery County", + "29141": "Morgan County", + "29143": "New Madrid County", + "29145": "Newton County", + "29147": "Nodaway County", + "29149": "Oregon County", + "29151": "Osage County", + "29153": "Ozark County", + "29155": "Pemiscot County", + "29157": "Perry County", + "29159": "Pettis County", + "29161": "Phelps County", + "29163": "Pike County", + "29165": "Platte County", + "29167": "Polk County", + "29169": "Pulaski County", + "29171": "Putnam County", + "29173": "Ralls County", + "29175": "Randolph County", + "29177": "Ray County", + "29179": "Reynolds County", + "29181": "Ripley County", + "29183": "St. Charles County", + "29185": "St. Clair County", + "29186": "Ste. Genevieve County", + "29187": "St. Francois County", + "29189": "St. Louis County", + "29195": "Saline County", + "29197": "Schuyler County", + "29199": "Scotland County", + "29201": "Scott County", + "29203": "Shannon County", + "29205": "Shelby County", + "29207": "Stoddard County", + "29209": "Stone County", + "29211": "Sullivan County", + "29213": "Taney County", + "29215": "Texas County", + "29217": "Vernon County", + "29219": "Warren County", + "29221": "Washington County", + "29223": "Wayne County", + "29225": "Webster County", + "29227": "Worth County", + "29229": "Wright County", + "29510": "St. Louis City", + "30001": "Beaverhead County", + "30003": "Big Horn County", + "30005": "Blaine County", + "30007": "Broadwater County", + "30009": "Carbon County", + "30011": "Carter County", + "30013": "Cascade County", + "30015": "Chouteau County", + "30017": "Custer County", + "30019": "Daniels County", + "30021": "Dawson County", + "30023": "Deer Lodge County", + "30025": "Fallon County", + "30027": "Fergus County", + "30029": "Flathead County", + "30031": "Gallatin County", + "30033": "Garfield County", + "30035": "Glacier County", + "30037": "Golden Valley County", + "30039": "Granite County", + "30041": "Hill County", + "30043": "Jefferson County", + "30045": "Judith Basin County", + "30047": "Lake County", + "30049": "Lewis and Clark County", + "30051": "Liberty County", + "30053": "Lincoln County", + "30055": "McCone County", + "30057": "Madison County", + "30059": "Meagher County", + "30061": "Mineral County", + "30063": "Missoula County", + "30065": "Musselshell County", + "30067": "Park County", + "30069": "Petroleum County", + "30071": "Phillips County", + "30073": "Pondera County", + "30075": "Powder River County", + "30077": "Powell County", + "30079": "Prairie County", + "30081": "Ravalli County", + "30083": "Richland County", + "30085": "Roosevelt County", + "30087": "Rosebud County", + "30089": "Sanders County", + "30091": "Sheridan County", + "30093": "Silver Bow County", + "30095": "Stillwater County", + "30097": "Sweet Grass County", + "30099": "Teton County", + "30101": "Toole County", + "30103": "Treasure County", + "30105": "Valley County", + "30107": "Wheatland County", + "30109": "Wibaux County", + "30111": "Yellowstone County", + "30113": "Yellowstone National Park - Part", + "31001": "Adams County", + "31003": "Antelope County", + "31005": "Arthur County", + "31007": "Banner County", + "31009": "Blaine County", + "31011": "Boone County", + "31013": "Box Butte County", + "31015": "Boyd County", + "31017": "Brown County", + "31019": "Buffalo County", + "31021": "Burt County", + "31023": "Butler County", + "31025": "Cass County", + "31027": "Cedar County", + "31029": "Chase County", + "31031": "Cherry County", + "31033": "Cheyenne County", + "31035": "Clay County", + "31037": "Colfax County", + "31039": "Cuming County", + "31041": "Custer County", + "31043": "Dakota County", + "31045": "Dawes County", + "31047": "Dawson County", + "31049": "Deuel County", + "31051": "Dixon County", + "31053": "Dodge County", + "31055": "Douglas County", + "31057": "Dundy County", + "31059": "Fillmore County", + "31061": "Franklin County", + "31063": "Frontier County", + "31065": "Furnas County", + "31067": "Gage County", + "31069": "Garden County", + "31071": "Garfield County", + "31073": "Gosper County", + "31075": "Grant County", + "31077": "Greeley County", + "31079": "Hall County", + "31081": "Hamilton County", + "31083": "Harlan County", + "31085": "Hayes County", + "31087": "Hitchcock County", + "31089": "Holt County", + "31091": "Hooker County", + "31093": "Howard County", + "31095": "Jefferson County", + "31097": "Johnson County", + "31099": "Kearney County", + "31101": "Keith County", + "31103": "Keya Paha County", + "31105": "Kimball County", + "31107": "Knox County", + "31109": "Lancaster County", + "31111": "Lincoln County", + "31113": "Logan County", + "31115": "Loup County", + "31117": "McPherson County", + "31119": "Madison County", + "31121": "Merrick County", + "31123": "Morrill County", + "31125": "Nance County", + "31127": "Nemaha County", + "31129": "Nuckolls County", + "31131": "Otoe County", + "31133": "Pawnee County", + "31135": "Perkins County", + "31137": "Phelps County", + "31139": "Pierce County", + "31141": "Platte County", + "31143": "Polk County", + "31145": "Red Willow County", + "31147": "Richardson County", + "31149": "Rock County", + "31151": "Saline County", + "31153": "Sarpy County", + "31155": "Saunders County", + "31157": "Scotts Bluff County", + "31159": "Seward County", + "31161": "Sheridan County", + "31163": "Sherman County", + "31165": "Sioux County", + "31167": "Stanton County", + "31169": "Thayer County", + "31171": "Thomas County", + "31173": "Thurston County", + "31175": "Valley County", + "31177": "Washington County", + "31179": "Wayne County", + "31181": "Webster County", + "31183": "Wheeler County", + "31185": "York County", + "32001": "Churchill County", + "32003": "Clark County", + "32005": "Douglas County", + "32007": "Elko County", + "32009": "Esmeralda County", + "32011": "Eureka County", + "32013": "Humboldt County", + "32015": "Lander County", + "32017": "Lincoln County", + "32019": "Lyon County", + "32021": "Mineral County", + "32023": "Nye County", + "32027": "Pershing County", + "32029": "Storey County", + "32031": "Washoe County", + "32033": "White Pine County", + "32510": "Carson City", + "33001": "Belknap County", + "33003": "Carroll County", + "33005": "Cheshire County", + "33007": "Coos County", + "33009": "Grafton County", + "33011": "Hillsborough County", + "33013": "Merrimack County", + "33015": "Rockingham County", + "33017": "Strafford County", + "33019": "Sullivan County", + "34001": "Atlantic County", + "34003": "Bergen County", + "34005": "Burlington County", + "34007": "Camden County", + "34009": "Cape May County", + "34011": "Cumberland County", + "34013": "Essex County", + "34015": "Gloucester County", + "34017": "Hudson County", + "34019": "Hunterdon County", + "34021": "Mercer County", + "34023": "Middlesex County", + "34025": "Monmouth County", + "34027": "Morris County", + "34029": "Ocean County", + "34031": "Passaic County", + "34033": "Salem County", + "34035": "Somerset County", + "34037": "Sussex County", + "34039": "Union County", + "34041": "Warren County", + "35001": "Bernalillo County", + "35003": "Catron County", + "35005": "Chaves County", + "35006": "Cibola County", + "35007": "Colfax County", + "35009": "Curry County", + "35011": "De Baca County", + "35013": "Dona Ana County", + "35015": "Eddy County", + "35017": "Grant County", + "35019": "Guadalupe County", + "35021": "Harding County", + "35023": "Hidalgo County", + "35025": "Lea County", + "35027": "Lincoln County", + "35028": "Los Alamos County", + "35029": "Luna County", + "35031": "McKinley County", + "35033": "Mora County", + "35035": "Otero County", + "35037": "Quay County", + "35039": "Rio Arriba County", + "35041": "Roosevelt County", + "35043": "Sandoval County", + "35045": "San Juan County", + "35047": "San Miguel County", + "35049": "Santa Fe County", + "35051": "Sierra County", + "35053": "Socorro County", + "35055": "Taos County", + "35057": "Torrance County", + "35059": "Union County", + "35061": "Valencia County", + "36001": "Albany County", + "36003": "Allegany County", + "36005": "Bronx County", + "36007": "Broome County", + "36009": "Cattaraugus County", + "36011": "Cayuga County", + "36013": "Chautauqua County", + "36015": "Chemung County", + "36017": "Chenango County", + "36019": "Clinton County", + "36021": "Columbia County", + "36023": "Cortland County", + "36025": "Delaware County", + "36027": "Dutchess County", + "36029": "Erie County", + "36031": "Essex County", + "36033": "Franklin County", + "36035": "Fulton County", + "36037": "Genesee County", + "36039": "Greene County", + "36041": "Hamilton County", + "36043": "Herkimer County", + "36045": "Jefferson County", + "36047": "Kings County", + "36049": "Lewis County", + "36051": "Livingston County", + "36053": "Madison County", + "36055": "Monroe County", + "36057": "Montgomery County", + "36059": "Nassau County", + "36061": "New York County", + "36063": "Niagara County", + "36065": "Oneida County", + "36067": "Onondaga County", + "36069": "Ontario County", + "36071": "Orange County", + "36073": "Orleans County", + "36075": "Oswego County", + "36077": "Otsego County", + "36079": "Putnam County", + "36081": "Queens County", + "36083": "Rensselaer County", + "36085": "Richmond County", + "36087": "Rockland County", + "36089": "St. Lawrence County", + "36091": "Saratoga County", + "36093": "Schenectady County", + "36095": "Schoharie County", + "36097": "Schuyler County", + "36099": "Seneca County", + "36101": "Steuben County", + "36103": "Suffolk County", + "36105": "Sullivan County", + "36107": "Tioga County", + "36109": "Tompkins County", + "36111": "Ulster County", + "36113": "Warren County", + "36115": "Washington County", + "36117": "Wayne County", + "36119": "Westchester County", + "36121": "Wyoming County", + "36123": "Yates County", + "37001": "Alamance County", + "37003": "Alexander County", + "37005": "Alleghany County", + "37007": "Anson County", + "37009": "Ashe County", + "37011": "Avery County", + "37013": "Beaufort County", + "37015": "Bertie County", + "37017": "Bladen County", + "37019": "Brunswick County", + "37021": "Buncombe County", + "37023": "Burke County", + "37025": "Cabarrus County", + "37027": "Caldwell County", + "37029": "Camden County", + "37031": "Carteret County", + "37033": "Caswell County", + "37035": "Catawba County", + "37037": "Chatham County", + "37039": "Cherokee County", + "37041": "Chowan County", + "37043": "Clay County", + "37045": "Cleveland County", + "37047": "Columbus County", + "37049": "Craven County", + "37051": "Cumberland County", + "37053": "Currituck County", + "37055": "Dare County", + "37057": "Davidson County", + "37059": "Davie County", + "37061": "Duplin County", + "37063": "Durham County", + "37065": "Edgecombe County", + "37067": "Forsyth County", + "37069": "Franklin County", + "37071": "Gaston County", + "37073": "Gates County", + "37075": "Graham County", + "37077": "Granville County", + "37079": "Greene County", + "37081": "Guilford County", + "37083": "Halifax County", + "37085": "Harnett County", + "37087": "Haywood County", + "37089": "Henderson County", + "37091": "Hertford County", + "37093": "Hoke County", + "37095": "Hyde County", + "37097": "Iredell County", + "37099": "Jackson County", + "37101": "Johnston County", + "37103": "Jones County", + "37105": "Lee County", + "37107": "Lenoir County", + "37109": "Lincoln County", + "37111": "McDowell County", + "37113": "Macon County", + "37115": "Madison County", + "37117": "Martin County", + "37119": "Mecklenburg County", + "37121": "Mitchell County", + "37123": "Montgomery County", + "37125": "Moore County", + "37127": "Nash County", + "37129": "New Hanover County", + "37131": "Northampton County", + "37133": "Onslow County", + "37135": "Orange County", + "37137": "Pamlico County", + "37139": "Pasquotank County", + "37141": "Pender County", + "37143": "Perquimans County", + "37145": "Person County", + "37147": "Pitt County", + "37149": "Polk County", + "37151": "Randolph County", + "37153": "Richmond County", + "37155": "Robeson County", + "37157": "Rockingham County", + "37159": "Rowan County", + "37161": "Rutherford County", + "37163": "Sampson County", + "37165": "Scotland County", + "37167": "Stanly County", + "37169": "Stokes County", + "37171": "Surry County", + "37173": "Swain County", + "37175": "Transylvania County", + "37177": "Tyrrell County", + "37179": "Union County", + "37181": "Vance County", + "37183": "Wake County", + "37185": "Warren County", + "37187": "Washington County", + "37189": "Watauga County", + "37191": "Wayne County", + "37193": "Wilkes County", + "37195": "Wilson County", + "37197": "Yadkin County", + "37199": "Yancey County", + "38001": "Adams County", + "38003": "Barnes County", + "38005": "Benson County", + "38007": "Billings County", + "38009": "Bottineau County", + "38011": "Bowman County", + "38013": "Burke County", + "38015": "Burleigh County", + "38017": "Cass County", + "38019": "Cavalier County", + "38021": "Dickey County", + "38023": "Divide County", + "38025": "Dunn County", + "38027": "Eddy County", + "38029": "Emmons County", + "38031": "Foster County", + "38033": "Golden Valley County", + "38035": "Grand Forks County", + "38037": "Grant County", + "38039": "Griggs County", + "38041": "Hettinger County", + "38043": "Kidder County", + "38045": "LaMoure County", + "38047": "Logan County", + "38049": "McHenry County", + "38051": "McIntosh County", + "38053": "McKenzie County", + "38055": "McLean County", + "38057": "Mercer County", + "38059": "Morton County", + "38061": "Mountrail County", + "38063": "Nelson County", + "38065": "Oliver County", + "38067": "Pembina County", + "38069": "Pierce County", + "38071": "Ramsey County", + "38073": "Ransom County", + "38075": "Renville County", + "38077": "Richland County", + "38079": "Rolette County", + "38081": "Sargent County", + "38083": "Sheridan County", + "38085": "Sioux County", + "38087": "Slope County", + "38089": "Stark County", + "38091": "Steele County", + "38093": "Stutsman County", + "38095": "Towner County", + "38097": "Traill County", + "38099": "Walsh County", + "38101": "Ward County", + "38103": "Wells County", + "38105": "Williams County", + "39001": "Adams County", + "39003": "Allen County", + "39005": "Ashland County", + "39007": "Ashtabula County", + "39009": "Athens County", + "39011": "Auglaize County", + "39013": "Belmont County", + "39015": "Brown County", + "39017": "Butler County", + "39019": "Carroll County", + "39021": "Champaign County", + "39023": "Clark County", + "39025": "Clermont County", + "39027": "Clinton County", + "39029": "Columbiana County", + "39031": "Coshocton County", + "39033": "Crawford County", + "39035": "Cuyahoga County", + "39037": "Darke County", + "39039": "Defiance County", + "39041": "Delaware County", + "39043": "Erie County", + "39045": "Fairfield County", + "39047": "Fayette County", + "39049": "Franklin County", + "39051": "Fulton County", + "39053": "Gallia County", + "39055": "Geauga County", + "39057": "Greene County", + "39059": "Guernsey County", + "39061": "Hamilton County", + "39063": "Hancock County", + "39065": "Hardin County", + "39067": "Harrison County", + "39069": "Henry County", + "39071": "Highland County", + "39073": "Hocking County", + "39075": "Holmes County", + "39077": "Huron County", + "39079": "Jackson County", + "39081": "Jefferson County", + "39083": "Knox County", + "39085": "Lake County", + "39087": "Lawrence County", + "39089": "Licking County", + "39091": "Logan County", + "39093": "Lorain County", + "39095": "Lucas County", + "39097": "Madison County", + "39099": "Mahoning County", + "39101": "Marion County", + "39103": "Medina County", + "39105": "Meigs County", + "39107": "Mercer County", + "39109": "Miami County", + "39111": "Monroe County", + "39113": "Montgomery County", + "39115": "Morgan County", + "39117": "Morrow County", + "39119": "Muskingum County", + "39121": "Noble County", + "39123": "Ottawa County", + "39125": "Paulding County", + "39127": "Perry County", + "39129": "Pickaway County", + "39131": "Pike County", + "39133": "Portage County", + "39135": "Preble County", + "39137": "Putnam County", + "39139": "Richland County", + "39141": "Ross County", + "39143": "Sandusky County", + "39145": "Scioto County", + "39147": "Seneca County", + "39149": "Shelby County", + "39151": "Stark County", + "39153": "Summit County", + "39155": "Trumbull County", + "39157": "Tuscarawas County", + "39159": "Union County", + "39161": "Van Wert County", + "39163": "Vinton County", + "39165": "Warren County", + "39167": "Washington County", + "39169": "Wayne County", + "39171": "Williams County", + "39173": "Wood County", + "39175": "Wyandot County", + "40001": "Adair County", + "40003": "Alfalfa County", + "40005": "Atoka County", + "40007": "Beaver County", + "40009": "Beckham County", + "40011": "Blaine County", + "40013": "Bryan County", + "40015": "Caddo County", + "40017": "Canadian County", + "40019": "Carter County", + "40021": "Cherokee County", + "40023": "Choctaw County", + "40025": "Cimarron County", + "40027": "Cleveland County", + "40029": "Coal County", + "40031": "Comanche County", + "40033": "Cotton County", + "40035": "Craig County", + "40037": "Creek County", + "40039": "Custer County", + "40041": "Delaware County", + "40043": "Dewey County", + "40045": "Ellis County", + "40047": "Garfield County", + "40049": "Garvin County", + "40051": "Grady County", + "40053": "Grant County", + "40055": "Greer County", + "40057": "Harmon County", + "40059": "Harper County", + "40061": "Haskell County", + "40063": "Hughes County", + "40065": "Jackson County", + "40067": "Jefferson County", + "40069": "Johnston County", + "40071": "Kay County", + "40073": "Kingfisher County", + "40075": "Kiowa County", + "40077": "Latimer County", + "40079": "Le Flore County", + "40081": "Lincoln County", + "40083": "Logan County", + "40085": "Love County", + "40087": "McClain County", + "40089": "McCurtain County", + "40091": "McIntosh County", + "40093": "Major County", + "40095": "Marshall County", + "40097": "Mayes County", + "40099": "Murray County", + "40101": "Muskogee County", + "40103": "Noble County", + "40105": "Nowata County", + "40107": "Okfuskee County", + "40109": "Oklahoma County", + "40111": "Okmulgee County", + "40113": "Osage County", + "40115": "Ottawa County", + "40117": "Pawnee County", + "40119": "Payne County", + "40121": "Pittsburg County", + "40123": "Pontotoc County", + "40125": "Pottawatomie County", + "40127": "Pushmataha County", + "40129": "Roger Mills County", + "40131": "Rogers County", + "40133": "Seminole County", + "40135": "Sequoyah County", + "40137": "Stephens County", + "40139": "Texas County", + "40141": "Tillman County", + "40143": "Tulsa County", + "40145": "Wagoner County", + "40147": "Washington County", + "40149": "Washita County", + "40151": "Woods County", + "40153": "Woodward County", + "41001": "Baker County", + "41003": "Benton County", + "41005": "Clackamas County", + "41007": "Clatsop County", + "41009": "Columbia County", + "41011": "Coos County", + "41013": "Crook County", + "41015": "Curry County", + "41017": "Deschutes County", + "41019": "Douglas County", + "41021": "Gilliam County", + "41023": "Grant County", + "41025": "Harney County", + "41027": "Hood River County", + "41029": "Jackson County", + "41031": "Jefferson County", + "41033": "Josephine County", + "41035": "Klamath County", + "41037": "Lake County", + "41039": "Lane County", + "41041": "Lincoln County", + "41043": "Linn County", + "41045": "Malheur County", + "41047": "Marion County", + "41049": "Morrow County", + "41051": "Multnomah County", + "41053": "Polk County", + "41055": "Sherman County", + "41057": "Tillamook County", + "41059": "Umatilla County", + "41061": "Union County", + "41063": "Wallowa County", + "41065": "Wasco County", + "41067": "Washington County", + "41069": "Wheeler County", + "41071": "Yamhill County", + "42001": "Adams County", + "42003": "Allegheny County", + "42005": "Armstrong County", + "42007": "Beaver County", + "42009": "Bedford County", + "42011": "Berks County", + "42013": "Blair County", + "42015": "Bradford County", + "42017": "Bucks County", + "42019": "Butler County", + "42021": "Cambria County", + "42023": "Cameron County", + "42025": "Carbon County", + "42027": "Centre County", + "42029": "Chester County", + "42031": "Clarion County", + "42033": "Clearfield County", + "42035": "Clinton County", + "42037": "Columbia County", + "42039": "Crawford County", + "42041": "Cumberland County", + "42043": "Dauphin County", + "42045": "Delaware County", + "42047": "Elk County", + "42049": "Erie County", + "42051": "Fayette County", + "42053": "Forest County", + "42055": "Franklin County", + "42057": "Fulton County", + "42059": "Greene County", + "42061": "Huntingdon County", + "42063": "Indiana County", + "42065": "Jefferson County", + "42067": "Juniata County", + "42069": "Lackawanna County", + "42071": "Lancaster County", + "42073": "Lawrence County", + "42075": "Lebanon County", + "42077": "Lehigh County", + "42079": "Luzerne County", + "42081": "Lycoming County", + "42083": "McKean County", + "42085": "Mercer County", + "42087": "Mifflin County", + "42089": "Monroe County", + "42091": "Montgomery County", + "42093": "Montour County", + "42095": "Northampton County", + "42097": "Northumberland County", + "42099": "Perry County", + "42101": "Philadelphia County", + "42103": "Pike County", + "42105": "Potter County", + "42107": "Schuylkill County", + "42109": "Snyder County", + "42111": "Somerset County", + "42113": "Sullivan County", + "42115": "Susquehanna County", + "42117": "Tioga County", + "42119": "Union County", + "42121": "Venango County", + "42123": "Warren County", + "42125": "Washington County", + "42127": "Wayne County", + "42129": "Westmoreland County", + "42131": "Wyoming County", + "42133": "York County", + "44001": "Bristol County", + "44003": "Kent County", + "44005": "Newport County", + "44007": "Providence County", + "44009": "Washington County", + "45001": "Abbeville County", + "45003": "Aiken County", + "45005": "Allendale County", + "45007": "Anderson County", + "45009": "Bamberg County", + "45011": "Barnwell County", + "45013": "Beaufort County", + "45015": "Berkeley County", + "45017": "Calhoun County", + "45019": "Charleston County", + "45021": "Cherokee County", + "45023": "Chester County", + "45025": "Chesterfield County", + "45027": "Clarendon County", + "45029": "Colleton County", + "45031": "Darlington County", + "45033": "Dillon County", + "45035": "Dorchester County", + "45037": "Edgefield County", + "45039": "Fairfield County", + "45041": "Florence County", + "45043": "Georgetown County", + "45045": "Greenville County", + "45047": "Greenwood County", + "45049": "Hampton County", + "45051": "Horry County", + "45053": "Jasper County", + "45055": "Kershaw County", + "45057": "Lancaster County", + "45059": "Laurens County", + "45061": "Lee County", + "45063": "Lexington County", + "45065": "McCormick County", + "45067": "Marion County", + "45069": "Marlboro County", + "45071": "Newberry County", + "45073": "Oconee County", + "45075": "Orangeburg County", + "45077": "Pickens County", + "45079": "Richland County", + "45081": "Saluda County", + "45083": "Spartanburg County", + "45085": "Sumter County", + "45087": "Union County", + "45089": "Williamsburg County", + "45091": "York County", + "46003": "Aurora County", + "46005": "Beadle County", + "46007": "Bennett County", + "46009": "Bon Homme County", + "46011": "Brookings County", + "46013": "Brown County", + "46015": "Brule County", + "46017": "Buffalo County", + "46019": "Butte County", + "46021": "Campbell County", + "46023": "Charles Mix County", + "46025": "Clark County", + "46027": "Clay County", + "46029": "Codington County", + "46031": "Corson County", + "46033": "Custer County", + "46035": "Davison County", + "46037": "Day County", + "46039": "Deuel County", + "46041": "Dewey County", + "46043": "Douglas County", + "46045": "Edmunds County", + "46047": "Fall River County", + "46049": "Faulk County", + "46051": "Grant County", + "46053": "Gregory County", + "46055": "Haakon County", + "46057": "Hamlin County", + "46059": "Hand County", + "46061": "Hanson County", + "46063": "Harding County", + "46065": "Hughes County", + "46067": "Hutchinson County", + "46069": "Hyde County", + "46071": "Jackson County", + "46073": "Jerauld County", + "46075": "Jones County", + "46077": "Kingsbury County", + "46079": "Lake County", + "46081": "Lawrence County", + "46083": "Lincoln County", + "46085": "Lyman County", + "46087": "McCook County", + "46089": "McPherson County", + "46091": "Marshall County", + "46093": "Meade County", + "46095": "Mellette County", + "46097": "Miner County", + "46099": "Minnehaha County", + "46101": "Moody County", + "46102": "Oglala Lakota County", + "46103": "Pennington County", + "46105": "Perkins County", + "46107": "Potter County", + "46109": "Roberts County", + "46111": "Sanborn County", + "46113": "Shannon County", + "46115": "Spink County", + "46117": "Stanley County", + "46119": "Sully County", + "46121": "Todd County", + "46123": "Tripp County", + "46125": "Turner County", + "46127": "Union County", + "46129": "Walworth County", + "46135": "Yankton County", + "46137": "Ziebach County", + "47001": "Anderson County", + "47003": "Bedford County", + "47005": "Benton County", + "47007": "Bledsoe County", + "47009": "Blount County", + "47011": "Bradley County", + "47013": "Campbell County", + "47015": "Cannon County", + "47017": "Carroll County", + "47019": "Carter County", + "47021": "Cheatham County", + "47023": "Chester County", + "47025": "Claiborne County", + "47027": "Clay County", + "47029": "Cocke County", + "47031": "Coffee County", + "47033": "Crockett County", + "47035": "Cumberland County", + "47037": "Davidson County", + "47039": "Decatur County", + "47041": "DeKalb County", + "47043": "Dickson County", + "47045": "Dyer County", + "47047": "Fayette County", + "47049": "Fentress County", + "47051": "Franklin County", + "47053": "Gibson County", + "47055": "Giles County", + "47057": "Grainger County", + "47059": "Greene County", + "47061": "Grundy County", + "47063": "Hamblen County", + "47065": "Hamilton County", + "47067": "Hancock County", + "47069": "Hardeman County", + "47071": "Hardin County", + "47073": "Hawkins County", + "47075": "Haywood County", + "47077": "Henderson County", + "47079": "Henry County", + "47081": "Hickman County", + "47083": "Houston County", + "47085": "Humphreys County", + "47087": "Jackson County", + "47089": "Jefferson County", + "47091": "Johnson County", + "47093": "Knox County", + "47095": "Lake County", + "47097": "Lauderdale County", + "47099": "Lawrence County", + "47101": "Lewis County", + "47103": "Lincoln County", + "47105": "Loudon County", + "47107": "McMinn County", + "47109": "McNairy County", + "47111": "Macon County", + "47113": "Madison County", + "47115": "Marion County", + "47117": "Marshall County", + "47119": "Maury County", + "47121": "Meigs County", + "47123": "Monroe County", + "47125": "Montgomery County", + "47127": "Moore County", + "47129": "Morgan County", + "47131": "Obion County", + "47133": "Overton County", + "47135": "Perry County", + "47137": "Pickett County", + "47139": "Polk County", + "47141": "Putnam County", + "47143": "Rhea County", + "47145": "Roane County", + "47147": "Robertson County", + "47149": "Rutherford County", + "47151": "Scott County", + "47153": "Sequatchie County", + "47155": "Sevier County", + "47157": "Shelby County", + "47159": "Smith County", + "47161": "Stewart County", + "47163": "Sullivan County", + "47165": "Sumner County", + "47167": "Tipton County", + "47169": "Trousdale County", + "47171": "Unicoi County", + "47173": "Union County", + "47175": "Van Buren County", + "47177": "Warren County", + "47179": "Washington County", + "47181": "Wayne County", + "47183": "Weakley County", + "47185": "White County", + "47187": "Williamson County", + "47189": "Wilson County", + "48001": "Anderson County", + "48003": "Andrews County", + "48005": "Angelina County", + "48007": "Aransas County", + "48009": "Archer County", + "48011": "Armstrong County", + "48013": "Atascosa County", + "48015": "Austin County", + "48017": "Bailey County", + "48019": "Bandera County", + "48021": "Bastrop County", + "48023": "Baylor County", + "48025": "Bee County", + "48027": "Bell County", + "48029": "Bexar County", + "48031": "Blanco County", + "48033": "Borden County", + "48035": "Bosque County", + "48037": "Bowie County", + "48039": "Brazoria County", + "48041": "Brazos County", + "48043": "Brewster County", + "48045": "Briscoe County", + "48047": "Brooks County", + "48049": "Brown County", + "48051": "Burleson County", + "48053": "Burnet County", + "48055": "Caldwell County", + "48057": "Calhoun County", + "48059": "Callahan County", + "48061": "Cameron County", + "48063": "Camp County", + "48065": "Carson County", + "48067": "Cass County", + "48069": "Castro County", + "48071": "Chambers County", + "48073": "Cherokee County", + "48075": "Childress County", + "48077": "Clay County", + "48079": "Cochran County", + "48081": "Coke County", + "48083": "Coleman County", + "48085": "Collin County", + "48087": "Collingsworth County", + "48089": "Colorado County", + "48091": "Comal County", + "48093": "Comanche County", + "48095": "Concho County", + "48097": "Cooke County", + "48099": "Coryell County", + "48101": "Cottle County", + "48103": "Crane County", + "48105": "Crockett County", + "48107": "Crosby County", + "48109": "Culberson County", + "48111": "Dallam County", + "48113": "Dallas County", + "48115": "Dawson County", + "48117": "Deaf Smith County", + "48119": "Delta County", + "48121": "Denton County", + "48123": "DeWitt County", + "48125": "Dickens County", + "48127": "Dimmit County", + "48129": "Donley County", + "48131": "Duval County", + "48133": "Eastland County", + "48135": "Ector County", + "48137": "Edwards County", + "48139": "Ellis County", + "48141": "El Paso County", + "48143": "Erath County", + "48145": "Falls County", + "48147": "Fannin County", + "48149": "Fayette County", + "48151": "Fisher County", + "48153": "Floyd County", + "48155": "Foard County", + "48157": "Fort Bend County", + "48159": "Franklin County", + "48161": "Freestone County", + "48163": "Frio County", + "48165": "Gaines County", + "48167": "Galveston County", + "48169": "Garza County", + "48171": "Gillespie County", + "48173": "Glasscock County", + "48175": "Goliad County", + "48177": "Gonzales County", + "48179": "Gray County", + "48181": "Grayson County", + "48183": "Gregg County", + "48185": "Grimes County", + "48187": "Guadalupe County", + "48189": "Hale County", + "48191": "Hall County", + "48193": "Hamilton County", + "48195": "Hansford County", + "48197": "Hardeman County", + "48199": "Hardin County", + "48201": "Harris County", + "48203": "Harrison County", + "48205": "Hartley County", + "48207": "Haskell County", + "48209": "Hays County", + "48211": "Hemphill County", + "48213": "Henderson County", + "48215": "Hidalgo County", + "48217": "Hill County", + "48219": "Hockley County", + "48221": "Hood County", + "48223": "Hopkins County", + "48225": "Houston County", + "48227": "Howard County", + "48229": "Hudspeth County", + "48231": "Hunt County", + "48233": "Hutchinson County", + "48235": "Irion County", + "48237": "Jack County", + "48239": "Jackson County", + "48241": "Jasper County", + "48243": "Jeff Davis County", + "48245": "Jefferson County", + "48247": "Jim Hogg County", + "48249": "Jim Wells County", + "48251": "Johnson County", + "48253": "Jones County", + "48255": "Karnes County", + "48257": "Kaufman County", + "48259": "Kendall County", + "48261": "Kenedy County", + "48263": "Kent County", + "48265": "Kerr County", + "48267": "Kimble County", + "48269": "King County", + "48271": "Kinney County", + "48273": "Kleberg County", + "48275": "Knox County", + "48277": "Lamar County", + "48279": "Lamb County", + "48281": "Lampasas County", + "48283": "La Salle County", + "48285": "Lavaca County", + "48287": "Lee County", + "48289": "Leon County", + "48291": "Liberty County", + "48293": "Limestone County", + "48295": "Lipscomb County", + "48297": "Live Oak County", + "48299": "Llano County", + "48301": "Loving County", + "48303": "Lubbock County", + "48305": "Lynn County", + "48307": "McCulloch County", + "48309": "McLennan County", + "48311": "McMullen County", + "48313": "Madison County", + "48315": "Marion County", + "48317": "Martin County", + "48319": "Mason County", + "48321": "Matagorda County", + "48323": "Maverick County", + "48325": "Medina County", + "48327": "Menard County", + "48329": "Midland County", + "48331": "Milam County", + "48333": "Mills County", + "48335": "Mitchell County", + "48337": "Montague County", + "48339": "Montgomery County", + "48341": "Moore County", + "48343": "Morris County", + "48345": "Motley County", + "48347": "Nacogdoches County", + "48349": "Navarro County", + "48351": "Newton County", + "48353": "Nolan County", + "48355": "Nueces County", + "48357": "Ochiltree County", + "48359": "Oldham County", + "48361": "Orange County", + "48363": "Palo Pinto County", + "48365": "Panola County", + "48367": "Parker County", + "48369": "Parmer County", + "48371": "Pecos County", + "48373": "Polk County", + "48375": "Potter County", + "48377": "Presidio County", + "48379": "Rains County", + "48381": "Randall County", + "48383": "Reagan County", + "48385": "Real County", + "48387": "Red River County", + "48389": "Reeves County", + "48391": "Refugio County", + "48393": "Roberts County", + "48395": "Robertson County", + "48397": "Rockwall County", + "48399": "Runnels County", + "48401": "Rusk County", + "48403": "Sabine County", + "48405": "San Augustine County", + "48407": "San Jacinto County", + "48409": "San Patricio County", + "48411": "San Saba County", + "48413": "Schleicher County", + "48415": "Scurry County", + "48417": "Shackelford County", + "48419": "Shelby County", + "48421": "Sherman County", + "48423": "Smith County", + "48425": "Somervell County", + "48427": "Starr County", + "48429": "Stephens County", + "48431": "Sterling County", + "48433": "Stonewall County", + "48435": "Sutton County", + "48437": "Swisher County", + "48439": "Tarrant County", + "48441": "Taylor County", + "48443": "Terrell County", + "48445": "Terry County", + "48447": "Throckmorton County", + "48449": "Titus County", + "48451": "Tom Green County", + "48453": "Travis County", + "48455": "Trinity County", + "48457": "Tyler County", + "48459": "Upshur County", + "48461": "Upton County", + "48463": "Uvalde County", + "48465": "Val Verde County", + "48467": "Van Zandt County", + "48469": "Victoria County", + "48471": "Walker County", + "48473": "Waller County", + "48475": "Ward County", + "48477": "Washington County", + "48479": "Webb County", + "48481": "Wharton County", + "48483": "Wheeler County", + "48485": "Wichita County", + "48487": "Wilbarger County", + "48489": "Willacy County", + "48491": "Williamson County", + "48493": "Wilson County", + "48495": "Winkler County", + "48497": "Wise County", + "48499": "Wood County", + "48501": "Yoakum County", + "48503": "Young County", + "48505": "Zapata County", + "48507": "Zavala County", + "49001": "Beaver County", + "49003": "Box Elder County", + "49005": "Cache County", + "49007": "Carbon County", + "49009": "Daggett County", + "49011": "Davis County", + "49013": "Duchesne County", + "49015": "Emery County", + "49017": "Garfield County", + "49019": "Grand County", + "49021": "Iron County", + "49023": "Juab County", + "49025": "Kane County", + "49027": "Millard County", + "49029": "Morgan County", + "49031": "Piute County", + "49033": "Rich County", + "49035": "Salt Lake County", + "49037": "San Juan County", + "49039": "Sanpete County", + "49041": "Sevier County", + "49043": "Summit County", + "49045": "Tooele County", + "49047": "Uintah County", + "49049": "Utah County", + "49051": "Wasatch County", + "49053": "Washington County", + "49055": "Wayne County", + "49057": "Weber County", + "50001": "Addison County", + "50003": "Bennington County", + "50005": "Caledonia County", + "50007": "Chittenden County", + "50009": "Essex County", + "50011": "Franklin County", + "50013": "Grand Isle County", + "50015": "Lamoille County", + "50017": "Orange County", + "50019": "Orleans County", + "50021": "Rutland County", + "50023": "Washington County", + "50025": "Windham County", + "50027": "Windsor County", + "51001": "Accomack County", + "51003": "Albemarle County", + "51005": "Alleghany County", + "51007": "Amelia County", + "51009": "Amherst County", + "51011": "Appomattox County", + "51013": "Arlington County", + "51015": "Augusta County", + "51017": "Bath County", + "51019": "Bedford County", + "51021": "Bland County", + "51023": "Botetourt County", + "51025": "Brunswick County", + "51027": "Buchanan County", + "51029": "Buckingham County", + "51031": "Campbell County", + "51033": "Caroline County", + "51035": "Carroll County", + "51036": "Charles City County", + "51037": "Charlotte County", + "51041": "Chesterfield County", + "51043": "Clarke County", + "51045": "Craig County", + "51047": "Culpeper County", + "51049": "Cumberland County", + "51051": "Dickenson County", + "51053": "Dinwiddie County", + "51057": "Essex County", + "51059": "Fairfax County", + "51061": "Fauquier County", + "51063": "Floyd County", + "51065": "Fluvanna County", + "51067": "Franklin County", + "51069": "Frederick County", + "51071": "Giles County", + "51073": "Gloucester County", + "51075": "Goochland County", + "51077": "Grayson County", + "51079": "Greene County", + "51081": "Greensville County", + "51083": "Halifax County", + "51085": "Hanover County", + "51087": "Henrico County", + "51089": "Henry County", + "51091": "Highland County", + "51093": "Isle of Wight County", + "51095": "James City County", + "51097": "King and Queen County", + "51099": "King George County", + "51101": "King William County", + "51103": "Lancaster County", + "51105": "Lee County", + "51107": "Loudoun County", + "51109": "Louisa County", + "51111": "Lunenburg County", + "51113": "Madison County", + "51115": "Mathews County", + "51117": "Mecklenburg County", + "51119": "Middlesex County", + "51121": "Montgomery County", + "51125": "Nelson County", + "51127": "New Kent County", + "51131": "Northampton County", + "51133": "Northumberland County", + "51135": "Nottoway County", + "51137": "Orange County", + "51139": "Page County", + "51141": "Patrick County", + "51143": "Pittsylvania County", + "51145": "Powhatan County", + "51147": "Prince Edward County", + "51149": "Prince George County", + "51153": "Prince William County", + "51155": "Pulaski County", + "51157": "Rappahannock County", + "51159": "Richmond County", + "51161": "Roanoke County", + "51163": "Rockbridge County", + "51165": "Rockingham County", + "51167": "Russell County", + "51169": "Scott County", + "51171": "Shenandoah County", + "51173": "Smyth County", + "51175": "Southampton County", + "51177": "Spotsylvania County", + "51179": "Stafford County", + "51181": "Surry County", + "51183": "Sussex County", + "51185": "Tazewell County", + "51187": "Warren County", + "51191": "Washington County", + "51193": "Westmoreland County", + "51195": "Wise County", + "51197": "Wythe County", + "51199": "York County", + "51510": "Alexandria City", + "51520": "Bristol City", + "51530": "Buena Vista City", + "51540": "Charlottesville City", + "51550": "Chesapeake City", + "51560": "Clifton Forge City", + "51570": "Colonial Heights City", + "51580": "Covington City", + "51590": "Danville City", + "51595": "Emporia City", + "51600": "Fairfax City", + "51610": "Falls Church City", + "51620": "Franklin City", + "51630": "Fredericksburg City", + "51640": "Galax City", + "51650": "Hampton City", + "51660": "Harrisonburg City", + "51670": "Hopewell City", + "51678": "Lexington City", + "51680": "Lynchburg City", + "51683": "Manassas City", + "51685": "Manassas Park City", + "51690": "Martinsville City", + "51700": "Newport News City", + "51710": "Norfolk City", + "51720": "Norton City", + "51730": "Petersburg City", + "51735": "Poquoson City", + "51740": "Portsmouth City", + "51750": "Radford City", + "51760": "Richmond City", + "51770": "Roanoke City", + "51775": "Salem City", + "51780": "South Boston City", + "51790": "Staunton City", + "51800": "Suffolk City", + "51810": "Virginia Beach City", + "51820": "Waynesboro City", + "51830": "Williamsburg City", + "51840": "Winchester City", + "53001": "Adams County", + "53003": "Asotin County", + "53005": "Benton County", + "53007": "Chelan County", + "53009": "Clallam County", + "53011": "Clark County", + "53013": "Columbia County", + "53015": "Cowlitz County", + "53017": "Douglas County", + "53019": "Ferry County", + "53021": "Franklin County", + "53023": "Garfield County", + "53025": "Grant County", + "53027": "Grays Harbor County", + "53029": "Island County", + "53031": "Jefferson County", + "53033": "King County", + "53035": "Kitsap County", + "53037": "Kittitas County", + "53039": "Klickitat County", + "53041": "Lewis County", + "53043": "Lincoln County", + "53045": "Mason County", + "53047": "Okanogan County", + "53049": "Pacific County", + "53051": "Pend Oreille County", + "53053": "Pierce County", + "53055": "San Juan County", + "53057": "Skagit County", + "53059": "Skamania County", + "53061": "Snohomish County", + "53063": "Spokane County", + "53065": "Stevens County", + "53067": "Thurston County", + "53069": "Wahkiakum County", + "53071": "Walla Walla County", + "53073": "Whatcom County", + "53075": "Whitman County", + "53077": "Yakima County", + "54001": "Barbour County", + "54003": "Berkeley County", + "54005": "Boone County", + "54007": "Braxton County", + "54009": "Brooke County", + "54011": "Cabell County", + "54013": "Calhoun County", + "54015": "Clay County", + "54017": "Doddridge County", + "54019": "Fayette County", + "54021": "Gilmer County", + "54023": "Grant County", + "54025": "Greenbrier County", + "54027": "Hampshire County", + "54029": "Hancock County", + "54031": "Hardy County", + "54033": "Harrison County", + "54035": "Jackson County", + "54037": "Jefferson County", + "54039": "Kanawha County", + "54041": "Lewis County", + "54043": "Lincoln County", + "54045": "Logan County", + "54047": "McDowell County", + "54049": "Marion County", + "54051": "Marshall County", + "54053": "Mason County", + "54055": "Mercer County", + "54057": "Mineral County", + "54059": "Mingo County", + "54061": "Monongalia County", + "54063": "Monroe County", + "54065": "Morgan County", + "54067": "Nicholas County", + "54069": "Ohio County", + "54071": "Pendleton County", + "54073": "Pleasants County", + "54075": "Pocahontas County", + "54077": "Preston County", + "54079": "Putnam County", + "54081": "Raleigh County", + "54083": "Randolph County", + "54085": "Ritchie County", + "54087": "Roane County", + "54089": "Summers County", + "54091": "Taylor County", + "54093": "Tucker County", + "54095": "Tyler County", + "54097": "Upshur County", + "54099": "Wayne County", + "54101": "Webster County", + "54103": "Wetzel County", + "54105": "Wirt County", + "54107": "Wood County", + "54109": "Wyoming County", + "55001": "Adams County", + "55003": "Ashland County", + "55005": "Barron County", + "55007": "Bayfield County", + "55009": "Brown County", + "55011": "Buffalo County", + "55013": "Burnett County", + "55015": "Calumet County", + "55017": "Chippewa County", + "55019": "Clark County", + "55021": "Columbia County", + "55023": "Crawford County", + "55025": "Dane County", + "55027": "Dodge County", + "55029": "Door County", + "55031": "Douglas County", + "55033": "Dunn County", + "55035": "Eau Claire County", + "55037": "Florence County", + "55039": "Fond du Lac County", + "55041": "Forest County", + "55043": "Grant County", + "55045": "Green County", + "55047": "Green Lake County", + "55049": "Iowa County", + "55051": "Iron County", + "55053": "Jackson County", + "55055": "Jefferson County", + "55057": "Juneau County", + "55059": "Kenosha County", + "55061": "Kewaunee County", + "55063": "La Crosse County", + "55065": "Lafayette County", + "55067": "Langlade County", + "55069": "Lincoln County", + "55071": "Manitowoc County", + "55073": "Marathon County", + "55075": "Marinette County", + "55077": "Marquette County", + "55078": "Menominee County", + "55079": "Milwaukee County", + "55081": "Monroe County", + "55083": "Oconto County", + "55085": "Oneida County", + "55087": "Outagamie County", + "55089": "Ozaukee County", + "55091": "Pepin County", + "55093": "Pierce County", + "55095": "Polk County", + "55097": "Portage County", + "55099": "Price County", + "55101": "Racine County", + "55103": "Richland County", + "55105": "Rock County", + "55107": "Rusk County", + "55109": "St. Croix County", + "55111": "Sauk County", + "55113": "Sawyer County", + "55115": "Shawano County", + "55117": "Sheboygan County", + "55119": "Taylor County", + "55121": "Trempealeau County", + "55123": "Vernon County", + "55125": "Vilas County", + "55127": "Walworth County", + "55129": "Washburn County", + "55131": "Washington County", + "55133": "Waukesha County", + "55135": "Waupaca County", + "55137": "Waushara County", + "55139": "Winnebago County", + "55141": "Wood County", + "56001": "Albany County", + "56003": "Big Horn County", + "56005": "Campbell County", + "56007": "Carbon County", + "56009": "Converse County", + "56011": "Crook County", + "56013": "Fremont County", + "56015": "Goshen County", + "56017": "Hot Springs County", + "56019": "Johnson County", + "56021": "Laramie County", + "56023": "Lincoln County", + "56025": "Natrona County", + "56027": "Niobrara County", + "56029": "Park County", + "56031": "Platte County", + "56033": "Sheridan County", + "56035": "Sublette County", + "56037": "Sweetwater County", + "56039": "Teton County", + "56041": "Uinta County", + "56043": "Washakie County", + "56045": "Weston County", + "60010": "Eastern District", + "60020": "Manu'a District", + "60030": "Rose Island", + "60040": "Swains Island", + "60050": "Western District", + "66010": "Guam", + "69085": "Northern Islands Municipality", + "69100": "Rota Municipality", + "69110": "Saipan Municipality", + "69120": "Tinian Municipality", + "72001": "Adjuntas Municipio", + "72003": "Aguada Municipio", + "72005": "Aguadilla Municipio", + "72007": "Aguas Buenas Municipio", + "72009": "Aibonito Municipio", + "72011": "Anasco Municipio", + "72013": "Arecibo Municipio", + "72015": "Arroyo Municipio", + "72017": "Barceloneta Municipio", + "72019": "Barranquitas Municipio", + "72021": "Bayamon Municipio", + "72023": "Cabo Rojo Municipio", + "72025": "Caguas Municipio", + "72027": "Camuy Municipio", + "72029": "Canovanas Municipio", + "72031": "Carolina Municipio", + "72033": "Catano Municipio", + "72035": "Cayey Municipio", + "72037": "Ceiba Municipio", + "72039": "Ciales Municipio", + "72041": "Cidra Municipio", + "72043": "Coamo Municipio", + "72045": "Comerio Municipio", + "72047": "Corozal Municipio", + "72049": "Culebra Municipio", + "72051": "Dorado Municipio", + "72053": "Fajardo Municipio", + "72054": "Florida Municipio", + "72055": "Guanica Municipio", + "72057": "Guayama Municipio", + "72059": "Guayanilla Municipio", + "72061": "Guaynabo Municipio", + "72063": "Gurabo Municipio", + "72065": "Hatillo Municipio", + "72067": "Hormigueros Municipio", + "72069": "Humacao Municipio", + "72071": "Isabela Municipio", + "72073": "Jayuya Municipio", + "72075": "Juana Diaz Municipio", + "72077": "Juncos Municipio", + "72079": "Lajas Municipio", + "72081": "Lares Municipio", + "72083": "Las Marias Municipio", + "72085": "Las Piedras Municipio", + "72087": "Loiza Municipio", + "72089": "Luquillo Municipio", + "72091": "Manati Municipio", + "72093": "Maricao Municipio", + "72095": "Maunabo Municipio", + "72097": "Mayaquez Municipio", + "72099": "Moca Municipio", + "72101": "Morovis Municipio", + "72103": "Naguabo Municipio", + "72105": "Naranjito Municipio", + "72107": "Orocovis Municipio", + "72109": "Patillas Municipio", + "72111": "Penuelas Municipio", + "72113": "Ponce Municipio", + "72115": "Quebradillas Municipio", + "72117": "Rincon Municipio", + "72119": "Rio Grande Municipio", + "72121": "Sabana Grande Municipio", + "72123": "Salinas Municipio", + "72125": "San German Municipio", + "72127": "San Juan Municipio", + "72129": "San Lorenzo Municipio", + "72131": "San Sebastian Municipio", + "72133": "Santa Isabel Municipio", + "72135": "Toa Alta Municipio", + "72137": "Toa Baja Municipio", + "72139": "Trujillo Alto Municipio", + "72141": "Utuado Municipio", + "72143": "Vega Alta Municipio", + "72145": "Vega Baja Municipio", + "72147": "Vieques Municipio", + "72149": "Villalba Municipio", + "72151": "Yabucoa Municipio", + "72153": "Yauco Municipio", + "78010": "St. Croix Island", + "78020": "St. John Island", + "78030": "St. Thomas Island", +} diff --git a/dataretrieval/codes/counties.py b/dataretrieval/codes/counties.py new file mode 100644 index 00000000..96d22463 --- /dev/null +++ b/dataretrieval/codes/counties.py @@ -0,0 +1,266 @@ +"""County code lookups and normalization, keyed by five-digit FIPS code. + +``counties`` maps each county or county equivalent -- parish, borough, census +area, independent city, municipio, Connecticut planning region -- to its name +as the Water Data API spells it (e.g. ``"55025": "Dane County"``). +:func:`to_county` normalizes a county identifier to a chosen representation, +and :func:`apply_county` resolves a getter's ``county`` argument into the +parameters its service filters on. An unrecognized value raises +``ValueError``. + +Coverage matches :mod:`dataretrieval.codes.states`: the 50 states, the +District of Columbia, and the five US territories. The table is a snapshot of +the Water Data ``counties`` collection, which follows the Census Bureau's +current codes; ``tests/utils_test.py`` compares the two live. +""" + +from __future__ import annotations + +import difflib +from collections.abc import Callable, Iterable +from typing import Any + +from dataretrieval._validation import reject_together, require_one_of + +from ._county_table import counties +from .states import to_state + +__all__ = ["apply_county", "counties", "to_county"] + +#: Trailing words that name a county's kind rather than the county, longest +#: first so "City and Borough" is stripped before "Borough". A caller may omit +#: them: ``"Dane"`` matches ``"Dane County"`` when nothing else in the state +#: shares the base name. +_DESIGNATIONS = ( + "city and borough", + "planning region", + "census area", + "municipality", + "municipio", + "borough", + "county", + "parish", + "island", + "district", + "city", +) + +_FORMATS = ("fips", "fips_us", "name") + + +def to_county( + value: str | int | Iterable[str | int], + to: str = "fips", + *, + state: str | int | Iterable[str | int] | None = None, +) -> str | list[str]: + """Normalize a US county identifier to a chosen representation. + + ``value`` may be given as + + * a five-digit FIPS code, as a string or integer (``"55025"``, ``55025``); + * a ``US:``-prefixed code with a colon between state and county + (``"US:55:025"``), as the Water Data statistics service writes it; + * with ``state``, a three-digit county code (``"025"``) or a name, matched + case-insensitively, with or without its designation (``"Dane County"`` + or ``"Dane"``). + + County names and three-digit codes repeat across states, so those forms + need ``state``, which takes any form :func:`~dataretrieval.codes.states.to_state` + accepts. A full FIPS code already names its state; given both, they must + agree. An iterable is resolved element-wise to a list. + + ``to`` selects the output representation: + + * ``"fips"`` -> five-digit FIPS code, e.g. ``"55025"`` + * ``"fips_us"`` -> ``"US:"`` + state + ``":"`` + county, e.g. ``"US:55:025"`` + * ``"name"`` -> the county's name, e.g. ``"Dane County"`` + + Raises + ------ + ValueError + If a value is not a county in the table, a name or three-digit code + has no ``state``, a bare name matches more than one county in the + state, or a FIPS code lies outside ``state``. + """ + require_one_of(to, _FORMATS, name="to") + state_fips = None if state is None else _single_state_fips(state) + if isinstance(value, (str, int)): + return _format_county(_to_fips_one(value, state_fips), to) + return [_format_county(_to_fips_one(v, state_fips), to) for v in value] + + +def apply_county( + local_vars: dict[str, Any], + *, + render: Callable[[list[str]], dict[str, Any]], + reject: tuple[str, ...], +) -> dict[str, Any]: + """Resolve a getter's ``county`` argument into its service's parameters. + + Pops ``county`` from ``local_vars`` (a no-op when absent) together with + ``state``, which then qualifies the counties instead of filtering on its + own: a county lies in one state, so sending both would be redundant at + best. The counties are resolved to five-digit FIPS codes, deduplicated in + order, and passed to ``render`` as a list, which returns the parameters + this service filters on; they are merged into ``local_vars``, which is + returned. + + ``reject`` names the getter's native state and county parameters. Passing + ``county`` with any of them raises ``ValueError``, and an unrecognized + county is re-raised naming them as the way to send the service's own + value. ``render`` may raise ``ValueError`` for a county its service cannot + filter on. + """ + county = local_vars.pop("county", None) + if county is None: + return local_vars + state = local_vars.pop("state", None) + reject_together( + {"county": county, **{p: local_vars.get(p) for p in reject}}, + context="they filter on the same thing", + ) + try: + fips = to_county(county, "fips", state=state) + except ValueError as err: + raise ValueError( + f"{err} Or pass {' or '.join(reject)} directly, using the API's " + "native value." + ) from err + local_vars.update(render(list(dict.fromkeys(_as_list(fips))))) + return local_vars + + +def _as_list(value: str | list[str]) -> list[str]: + """``to_county``'s result as a list, whether it was given one value or many.""" + return value if isinstance(value, list) else [value] + + +def _single_state_fips(state: object) -> str: + """The two-digit FIPS code of the one state qualifying ``county``.""" + if not isinstance(state, (str, int)): + raise ValueError( + "With county, state names the one state the counties are in, so it " + f"must be a single state (got {state!r}). To select counties in " + "several states, pass each as a five-digit FIPS code, e.g. " + "county=['55025', '17031'], without state." + ) + fips = to_state(state, "fips") + assert isinstance(fips, str) + return fips + + +def _to_fips_one(value: str | int, state_fips: str | None) -> str: + """Resolve one county identifier to its five-digit FIPS code.""" + if isinstance(value, bool): + raise _unrecognized(value) + s = str(value).strip() + fips = _parse_code(s, state_fips) + if fips is None: + if state_fips is None: + raise ValueError( + f"county={value!r} is a name, and county names repeat across " + f"states. Pass the state too, e.g. county={value!r}, state='WI', " + "or a five-digit FIPS code, e.g. county='55025'." + ) + fips = _match_name(s, state_fips) + if fips not in counties: + raise _unrecognized(value, in_connecticut=fips.startswith("09")) + if state_fips is not None and fips[:2] != state_fips: + raise ValueError( + f"county={value!r} is in {to_state(fips[:2], 'name')}, not " + f"{to_state(state_fips, 'name')}. Drop state, or pass a county in " + "that state." + ) + return fips + + +def _parse_code(s: str, state_fips: str | None) -> str | None: + """The FIPS code a code-shaped identifier spells, or ``None`` for a name. + + Not checked against the table; the caller does that once for every form. + """ + if s[:3].upper() == "US:": + return s[3:].replace(":", "") + if not s.isdigit(): + return None + if len(s) == 3 and state_fips is not None: + return state_fips + s + # An integer, or a string that lost its leading zero (``1001``). Any other + # length is returned as given, so the table lookup rejects it. + return s.zfill(5) if len(s) == 4 else s + + +def _normal(name: str) -> str: + """``name`` lower-cased, without periods, with single spaces: ``"St Louis"`` + and ``"st. louis"`` compare equal.""" + return " ".join(name.lower().replace(".", "").split()) + + +def _base(name: str) -> str: + """``name`` normalized, without its trailing designation.""" + lowered = _normal(name) + for word in _DESIGNATIONS: + if lowered.endswith(f" {word}"): + return lowered[: -len(word) - 1] + return lowered + + +def _match_name(name: str, state_fips: str) -> str: + """The FIPS code of the county called ``name`` in the state.""" + in_state = {f: n for f, n in counties.items() if f[:2] == state_fips} + wanted = _normal(name) + by_name = {_normal(n): f for f, n in in_state.items()} + if wanted in by_name: + return by_name[wanted] + # The input may itself end in a designation word that is part of the + # name ("Charles City" for Charles City County), so try it both ways. + keys = {wanted, _base(wanted)} + by_base = [f for f, n in in_state.items() if _base(n) in keys] + if len(by_base) == 1: + return by_base[0] + state_name = to_state(state_fips, "name") + if by_base: + options = ", ".join(repr(in_state[f]) for f in by_base) + raise ValueError( + f"county={name!r} matches more than one county in {state_name}: " + f"{options}. Pass the full name." + ) + raise ValueError( + f"county={name!r} is not a county in {state_name}." + f"{_suggest(wanted, in_state.values())}" + ) + + +def _suggest(wanted: str, names: Iterable[str]) -> str: + """A "Did you mean" hint naming the counties closest to ``wanted``.""" + bases: dict[str, list[str]] = {} + for n in names: + bases.setdefault(_base(n), []).append(n) + close = difflib.get_close_matches(_base(wanted), list(bases)) + suggestions = [repr(n) for c in close for n in bases[c]] + return f" Did you mean {', '.join(suggestions)}?" if suggestions else "" + + +def _unrecognized(value: object, *, in_connecticut: bool = False) -> ValueError: + """The error for a code that names no county in the table.""" + hint = ( + " Connecticut's counties were replaced in 2022 by planning regions, " + "codes 09110 to 09190." + if in_connecticut + else "" + ) + return ValueError( + f"{value!r} is not a recognized US county or county equivalent. Pass a " + "five-digit FIPS code (county='55025'), or a name with its state " + f"(county='Dane County', state='WI').{hint}" + ) + + +def _format_county(fips: str, to: str) -> str: + """Render a five-digit FIPS code in the ``to`` representation.""" + if to == "fips": + return fips + if to == "fips_us": + return f"US:{fips[:2]}:{fips[2:]}" + return counties[fips] diff --git a/dataretrieval/codes/states.py b/dataretrieval/codes/states.py index e0d828d2..ea32ccad 100644 --- a/dataretrieval/codes/states.py +++ b/dataretrieval/codes/states.py @@ -72,7 +72,7 @@ "Guam": "gu", "Northern Mariana Islands": "mp", "Puerto Rico": "pr", - "US Virgin Islands": "vi", + "Virgin Islands": "vi", } fips_codes = { @@ -131,7 +131,7 @@ "Guam": "66", "Northern Mariana Islands": "69", "Puerto Rico": "72", - "US Virgin Islands": "78", + "Virgin Islands": "78", } # Reverse lookups (built once): postal code -> name, FIPS code -> name, and a @@ -140,6 +140,9 @@ _name_by_postal = {code: name for name, code in state_codes.items()} _name_by_fips = {fips: name for name, fips in fips_codes.items()} _name_by_lower = {name.lower(): name for name in state_codes} +# The table names each state as the Water Data and NGWMN ``state_name`` fields +# do. "US Virgin Islands" is the Census Bureau's name, accepted as input only. +_name_by_lower["us virgin islands"] = "Virgin Islands" def to_state( diff --git a/dataretrieval/ngwmn.py b/dataretrieval/ngwmn.py index ced8cc79..86bc5f75 100644 --- a/dataretrieval/ngwmn.py +++ b/dataretrieval/ngwmn.py @@ -24,7 +24,8 @@ import pandas as pd from dataretrieval import configuration as _configuration -from dataretrieval.codes.states import apply_state +from dataretrieval.codes.counties import apply_county, counties +from dataretrieval.codes.states import apply_state, to_state from dataretrieval.configuration import ( BaseConfiguration, _Chunked, @@ -80,11 +81,50 @@ # (there is no service-specific id name as there is for the main collections). _NGWMN_OUTPUT_ID = "id" -# NGWMN's request shape matches the generic OGC default (no CQL2-only or -# date-only collections), but its result columns need their own coercion and -# sort vocabulary: water-level observations are timestamped by ``sample_time`` -# (not the Water Data ``time``) and report depths/levels in feet. +# --- county-filter shim ------------------------------------------------------ +# ``sites`` files a county under ``state_name`` and ``county_name``. Its names +# are the Water Data ``counties`` names that ``codes.counties`` holds, with two +# exceptions: Anchorage is named the other way round, and Connecticut sites +# keep the eight counties the state replaced in 2022 with planning regions, +# which do not map one to one onto them. +# ``tests/ngwmn_test.py::test_county_names_still_match_the_table_upstream`` fails +# if that changes. +_NGWMN_COUNTY_NAMES = {"02020": "Municipality of Anchorage"} +_LEGACY_COUNTY_STATES = frozenset({"09"}) + + +def _sites_county_params(fips: list[str]) -> dict[str, Any]: + """``state_name`` plus ``county_name`` for counties in one state.""" + states = sorted({f[:2] for f in fips}) + if len(states) > 1: + # A cross-state set needs the pairs OR-ed, which NGWMN's edge refuses as + # a CQL text filter (HTTP 403); separate state_name and county_name + # lists would also match a same-named county in another listed state. + raise ValueError( + "ngwmn.get_sites takes counties from one state per call, but county " + f"names counties in {len(states)} states. Call get_sites once per " + "state and concatenate the results." + ) + if states[0] in _LEGACY_COUNTY_STATES: + raise ValueError( + f"NGWMN still files {to_state(states[0], 'name')} sites under the " + "counties it replaced in 2022, which no planning region code " + "matches. Pass county_name directly, e.g. county_name='Hartford " + f"County', state={to_state(states[0], 'postal')!r}." + ) + return { + "state_name": to_state(states[0], "name"), + "county_name": [_NGWMN_COUNTY_NAMES.get(f, counties[f]) for f in fips], + } + + +# NGWMN has no date-only collections. ``sites`` takes multi-value filters as a +# CQL2 POST: a comma-joined ``county_name`` matches nothing, where the same +# names POSTed match. Its result columns need their own coercion and sort +# vocabulary: water-level observations are timestamped by ``sample_time`` (not +# the Water Data ``time``) and report depths/levels in feet. NGWMN_DIALECT = OgcDialect( + cql2_services=frozenset({"sites"}), time_cols=frozenset({"sample_time"}), numerical_cols=frozenset( { @@ -103,6 +143,8 @@ def _get(service: str, local_vars: dict[str, Any]) -> tuple[pd.DataFrame, BaseMe Every NGWMN getter ends with this same call; centralizing it keeps the NGWMN base URL, output id, and dialect set in one place. """ + if service == "sites": + apply_county(local_vars, render=_sites_county_params, reject=("county_name",)) queryable = _STATE_QUERYABLE.get(service) if queryable is not None: apply_state(local_vars, to=queryable["to"], into=queryable["into"]) @@ -131,6 +173,7 @@ def get_sites( country_code: str | Iterable[str] | None = None, country_name: str | Iterable[str] | None = None, state: str | Iterable[str] | None = None, + county: str | int | Iterable[str | int] | None = None, county_name: str | Iterable[str] | None = None, aquifer_name: str | Iterable[str] | None = None, site_type: str | Iterable[str] | None = None, @@ -178,8 +221,19 @@ def get_sites( State filter. Accepts a full name (``"Wisconsin"``), a two-letter postal code (``"WI"``), or a two-digit ANSI/FIPS code (``"55"``). The 50 states, DC, and the five US territories. + county : str, int, or iterable of them, optional + County filter (the recommended parameter). Accepts a five-digit FIPS + code (``"55025"`` or ``55025``), the ``"US:55:025"`` form, or, with + ``state``, a name (``"Dane County"`` or ``"Dane"``) or three-digit + county code (``"025"``). A dataretrieval argument rather than an API + field: it is sent as the API's ``state_name`` and ``county_name``, and + ``state``, if given, names the one state the counties are in rather + than filtering separately. One state per call. Connecticut raises + ``ValueError``: NGWMN still files its sites under the counties replaced + in 2022, so pass ``county_name`` instead. See + ``dataretrieval.codes.to_county``. county_name : str or iterable of str, optional - County name filter. + County name filter, as NGWMN spells it (``"Dane County"``). aquifer_name, site_type, aquifer_type_code : str or iterable, optional Aquifer name, site type, and aquifer-type code. qw_sys_name, qw_sn_flag, qw_baseline_flag : str or iterable, optional diff --git a/dataretrieval/nwdc.py b/dataretrieval/nwdc.py index a42b8254..8ee87295 100644 --- a/dataretrieval/nwdc.py +++ b/dataretrieval/nwdc.py @@ -49,6 +49,7 @@ from dataretrieval._querying import _raise_for_status, to_str from dataretrieval._response_metadata import BaseMetadata from dataretrieval._validation import render_options, require_exactly_one +from dataretrieval.codes.counties import to_county from dataretrieval.codes.states import to_state from dataretrieval.configuration import ( BaseConfiguration, @@ -104,7 +105,7 @@ def get_wateruse( model: str, variable: str | Iterable[str] | None = None, state: str | int | Iterable[str | int] | None = None, - county: str | Iterable[str] | None = None, + county: str | int | Iterable[str | int] | None = None, huc: str | Iterable[str] | None = None, time_resolution: str | None = None, start_date: str | None = None, @@ -153,9 +154,14 @@ def get_wateruse( One or more US states/territories to query. Each accepts a full name (``"Wisconsin"``), a two-letter postal code (``"WI"``), or a two-digit ANSI/FIPS code (``"55"`` or ``55``), matching :func:`dataretrieval.ngwmn.get_sites`. - county : string or iterable, optional - One or more five-digit county FIPS codes — state FIPS + county FIPS, - e.g. ``"55025"`` for Dane County, Wisconsin. + county : string, int, or iterable, optional + One or more counties. Each accepts a five-digit FIPS code — state FIPS + + county FIPS, e.g. ``"55025"`` or ``55025`` for Dane County, Wisconsin — + the ``"US:55:025"`` form, or, with ``state``, a name (``"Dane County"`` + or ``"Dane"``) or three-digit county code (``"025"``). With ``county``, + ``state`` names the one state the counties are in rather than a second + area. Sent as the API's ``countyCd:`` location. See + ``dataretrieval.codes.to_county``. huc : string or iterable, optional One or more hydrologic unit codes. Each code's level is taken from its length: a 2-digit code queries a HUC2 region, 8-digit a HUC8 subbasin, @@ -163,7 +169,8 @@ def get_wateruse( ``"07070005"``, ``"010900020502"``). Provide exactly one of ``state``, ``county``, or ``huc`` (each may be a - single value or a list). + single value or a list); ``state`` may accompany ``county`` to qualify + it. time_resolution : string, optional Temporal resolution: ``"monthly"``, ``"annualcy"`` (annual, calendar year), or ``"annualwy"`` (annual, water year). See @@ -199,8 +206,8 @@ def get_wateruse( ------ ValueError If not exactly one of ``state``, ``county``, or ``huc`` is given, or a - given selector is malformed (an unrecognized state, a county code that - is not five digits, or a HUC of invalid length). + given selector is malformed (an unrecognized state or county, or a HUC + of invalid length). DataRetrievalError On an HTTP error response, the typed subclass for the status (see :func:`dataretrieval.exceptions.error_for_status`). A transient 429, @@ -273,24 +280,27 @@ def get_wateruse( # per location (see :func:`_fan_out`). _LOCATION_BUILDERS: dict[str, Callable[[Any], list[str]]] = { "state": lambda v: [f"stateCd:{c}" for c in _as_list(to_state(v, to="postal"))], - "county": lambda v: [f"countyCd:{_validate_county(c)}" for c in _as_list(v)], + "county": lambda v: [f"countyCd:{c}" for c in _as_list(v)], "huc": lambda v: [f"huc{len(c)}:{c}" for c in map(_validate_huc, _as_list(v))], } def _resolve_locations( state: str | int | Iterable[str | int] | None, - county: str | Iterable[str] | None, + county: str | int | Iterable[str | int] | None, huc: str | Iterable[str] | None, ) -> list[str]: """Build the NWDC ``location=:`` value(s) from the selectors. Exactly one of ``state`` / ``county`` / ``huc`` must be given; each may be a single value or a list. ``state`` is normalized to the two-letter postal - code ``stateCd`` requires; ``county`` is a five-digit FIPS code; and a + code ``stateCd`` requires; ``county`` to a five-digit FIPS code, with + ``state`` qualifying it rather than counting as a second location; and a ``huc`` code's length selects its level (``huc2`` … ``huc12``). Returns one location string per value — the caller issues one request per location. """ + if county is not None: + county, state = to_county(county, "fips", state=state), None selectors = {"state": state, "county": county, "huc": huc} name, value = require_exactly_one(selectors, context="as the query's location") locations = _LOCATION_BUILDERS[name](value) @@ -313,17 +323,6 @@ def _as_list(value: object) -> list[Any]: return [value] -def _validate_county(value: object) -> str: - """Validate and normalize a five-digit state+county FIPS code.""" - code = str(value).strip() - if not (code.isdigit() and len(code) == 5): - raise ValueError( - "county must be a five-digit state+county FIPS code " - f"(e.g. '55025'), got {value!r}." - ) - return code - - def _validate_huc(value: object) -> str: """Validate a HUC code (even length 2-12 digits; level set by length).""" code = str(value).strip() diff --git a/dataretrieval/waterdata/metadata.py b/dataretrieval/waterdata/metadata.py index eb26d98e..5acdb93c 100644 --- a/dataretrieval/waterdata/metadata.py +++ b/dataretrieval/waterdata/metadata.py @@ -15,6 +15,7 @@ from dataretrieval.waterdata.utils import ( _get_args, + _with_location_county, _with_state, get_ogc_data, ) @@ -36,6 +37,7 @@ def get_monitoring_locations( state: str | Iterable[str] | None = None, state_code: str | Iterable[str] | None = None, state_name: str | Iterable[str] | None = None, + county: str | int | Iterable[str | int] | None = None, county_code: str | Iterable[str] | None = None, county_name: str | Iterable[str] | None = None, minor_civil_division_code: str | Iterable[str] | None = None, @@ -133,6 +135,17 @@ def get_monitoring_locations( state_name : string or iterable of strings, optional The name of the state or state equivalent in which the monitoring location is located. + county : string, integer, or iterable of them, optional + County filter (the recommended parameter). Accepts a five-digit FIPS + code (``"55025"`` or ``55025``), the ``"US:55:025"`` form, or, with + ``state``, a name (``"Dane County"`` or + ``"Dane"``) or three-digit county code (``"025"``). A dataretrieval + argument rather than an API field: it is sent as the API's + ``state_code`` and ``county_code``, and ``state``, if given, names the + one state the counties are in rather than filtering separately. + Counties in more than one state are sent as a CQL ``filter``, so they + cannot be combined with ``filter``. See + ``dataretrieval.codes.to_county``. county_code : string or iterable of strings, optional The code for the county or county equivalent (parish, borough, etc.) in which the monitoring location is located. A `list of codes @@ -337,7 +350,8 @@ def get_monitoring_locations( # Build argument dictionary, omitting None values (resolving the unified # `state` argument into the OGC `state_name` queryable). args = _get_args( - _with_state(locals(), to="name", into="state_name"), exclude={"max_rows"} + _with_state(_with_location_county(locals()), to="name", into="state_name"), + exclude={"max_rows"}, ) return get_ogc_data(args, collection, max_rows=max_rows) @@ -621,6 +635,7 @@ def get_combined_metadata( state: str | Iterable[str] | None = None, state_code: str | Iterable[str] | None = None, state_name: str | Iterable[str] | None = None, + county: str | int | Iterable[str | int] | None = None, county_code: str | Iterable[str] | None = None, county_name: str | Iterable[str] | None = None, minor_civil_division_code: str | Iterable[str] | None = None, @@ -733,6 +748,17 @@ def get_combined_metadata( State/territory filter (the recommended parameter). Accepts a full name (``"Wisconsin"``), a two-letter postal code (``"WI"``), or a two-digit ANSI/FIPS code (``"55"``). + county : string, integer, or iterable of them, optional + County filter (the recommended parameter). Accepts a five-digit FIPS + code (``"55025"`` or ``55025``), the ``"US:55:025"`` form, or, with + ``state``, a name (``"Dane County"`` or + ``"Dane"``) or three-digit county code (``"025"``). A dataretrieval + argument rather than an API field: it is sent as the API's + ``state_code`` and ``county_code``, and ``state``, if given, names the + one state the counties are in rather than filtering separately. + Counties in more than one state are sent as a CQL ``filter``, so they + cannot be combined with ``filter``. See + ``dataretrieval.codes.to_county``. state_name, county_name, hydrologic_unit_code, site_type, \ site_type_code : string or iterable of strings, optional Common location-catalog filters shared with the @@ -846,7 +872,8 @@ def get_combined_metadata( # Resolve the unified `state` argument into the OGC `state_name` queryable. args = _get_args( - _with_state(locals(), to="name", into="state_name"), exclude={"max_rows"} + _with_state(_with_location_county(locals()), to="name", into="state_name"), + exclude={"max_rows"}, ) return get_ogc_data(args, collection, max_rows=max_rows) diff --git a/dataretrieval/waterdata/time_series.py b/dataretrieval/waterdata/time_series.py index c83232f0..6c8d685d 100644 --- a/dataretrieval/waterdata/time_series.py +++ b/dataretrieval/waterdata/time_series.py @@ -20,6 +20,7 @@ from dataretrieval.waterdata.utils import ( _get_args, _with_state, + _with_statistics_county, get_ogc_data, ) @@ -884,6 +885,7 @@ def get_stats_por( country_code: str | Iterable[str] | None = None, state: str | Iterable[str] | None = None, state_code: str | Iterable[str] | None = None, + county: str | int | Iterable[str | int] | None = None, county_code: str | Iterable[str] | None = None, start_date: str | None = None, end_date: str | None = None, @@ -925,6 +927,14 @@ def get_stats_por( state_code: string, optional State query parameter. Takes the format "US:XX", where XX is the two-digit state code. API defaults to "US:42" (Pennsylvania). + county: string, integer, or iterable of them, optional + County filter (the recommended parameter). Accepts a five-digit FIPS + code ("55025" or 55025), the "US:55:025" form, or, with ``state``, a + name ("Dane County" or "Dane") or three-digit county code ("025"). A + dataretrieval argument rather than an API field: it is sent as the + API's ``county_code``, in its "US:XX:YYY" format, and ``state``, if + given, names the one state the counties are in rather than filtering + separately. See ``dataretrieval.codes.to_county``. county_code: string, optional County query parameter. Takes the format "US:XX:YYY", where XX is the two-digit state code and YYY is the three-digit county code. @@ -1009,7 +1019,7 @@ def get_stats_por( """ # Build argument dictionary, omitting None values params = _get_args( - _with_state(locals(), to="fips_us", into="state_code"), + _with_state(_with_statistics_county(locals()), to="fips_us", into="state_code"), exclude={"expand_percentiles"}, ) @@ -1024,6 +1034,7 @@ def get_stats_date_range( country_code: str | Iterable[str] | None = None, state: str | Iterable[str] | None = None, state_code: str | Iterable[str] | None = None, + county: str | int | Iterable[str | int] | None = None, county_code: str | Iterable[str] | None = None, start_date: str | None = None, end_date: str | None = None, @@ -1065,6 +1076,14 @@ def get_stats_date_range( state_code: string, optional State query parameter. Takes the format "US:XX", where XX is the two-digit state code. API defaults to "US:42" (Pennsylvania). + county: string, integer, or iterable of them, optional + County filter (the recommended parameter). Accepts a five-digit FIPS + code ("55025" or 55025), the "US:55:025" form, or, with ``state``, a + name ("Dane County" or "Dane") or three-digit county code ("025"). A + dataretrieval argument rather than an API field: it is sent as the + API's ``county_code``, in its "US:XX:YYY" format, and ``state``, if + given, names the one state the counties are in rather than filtering + separately. See ``dataretrieval.codes.to_county``. county_code: string, optional County query parameter. Takes the format "US:XX:YYY", where XX is the two-digit state code and YYY is the three-digit county code. @@ -1155,7 +1174,7 @@ def get_stats_date_range( """ # Build argument dictionary, omitting None values params = _get_args( - _with_state(locals(), to="fips_us", into="state_code"), + _with_state(_with_statistics_county(locals()), to="fips_us", into="state_code"), exclude={"expand_percentiles"}, ) diff --git a/dataretrieval/waterdata/utils.py b/dataretrieval/waterdata/utils.py index ede2e117..49df745d 100644 --- a/dataretrieval/waterdata/utils.py +++ b/dataretrieval/waterdata/utils.py @@ -22,6 +22,8 @@ import pandas as pd from dataretrieval._deprecation import warn_deprecated +from dataretrieval._validation import reject_together +from dataretrieval.codes.counties import apply_county from dataretrieval.codes.states import apply_state from dataretrieval.credentials import refuse_credential_keywords from dataretrieval.ogc import OgcDialect, prepare_request_args @@ -189,6 +191,56 @@ def _with_state(local_vars: dict[str, Any], *, to: str, into: str) -> dict[str, ) +def _with_location_county(local_vars: dict[str, Any]) -> dict[str, Any]: + """Resolve the unified ``county`` argument for the location collections. + + ``monitoring-locations`` and ``combined-metadata`` file a county under two + fields, ``state_code`` and a three-digit ``county_code`` that repeats + across states, so counties are sent as (state, county) pairs. Counties in + one state go out as ``state_code`` plus a ``county_code`` list; counties in + several need a CQL ``filter`` OR-ing the pairs, which the request planner + splits at its top-level ``OR`` when it outgrows the request size cap. + """ + _flatten_queryables(local_vars) + user_filter = local_vars.get("filter") + return apply_county( + local_vars, + render=lambda fips: _location_county_params(fips, user_filter), + reject=("state_code", "state_name", "county_code", "county_name"), + ) + + +def _location_county_params(fips: list[str], user_filter: object) -> dict[str, Any]: + """The location collections' parameters for counties ``fips``.""" + states = {f[:2] for f in fips} + if len(states) == 1: + return {"state_code": states.pop(), "county_code": [f[2:] for f in fips]} + reject_together( + {"county": fips, "filter": user_filter}, + context="when the counties are in more than one state, which is sent " + "as a filter", + remedy="Pass the counties of one state per call, or write the counties " + "into filter yourself, e.g. \"(state_code='55' AND county_code='025') OR " + "(state_code='17' AND county_code='031')\".", + ) + return { + "filter": " OR ".join( + f"(state_code='{f[:2]}' AND county_code='{f[2:]}')" for f in fips + ) + } + + +def _with_statistics_county(local_vars: dict[str, Any]) -> dict[str, Any]: + """Resolve the unified ``county`` argument for the statistics service, + which takes each county as one ``county_code`` of the form ``US:55:025``.""" + _flatten_queryables(local_vars) + return apply_county( + local_vars, + render=lambda fips: {"county_code": [f"US:{f[:2]}:{f[2:]}" for f in fips]}, + reject=("state_code", "county_code"), + ) + + def get_ogc_data( args: dict[str, Any], collection: str, diff --git a/docs/source/architecture/decisions/0013-core-and-domain-terms.rst b/docs/source/architecture/decisions/0013-core-and-domain-terms.rst index 105b35d2..36535c61 100644 --- a/docs/source/architecture/decisions/0013-core-and-domain-terms.rst +++ b/docs/source/architecture/decisions/0013-core-and-domain-terms.rst @@ -6,6 +6,9 @@ Status Accepted +Amended after acceptance under :doc:`0000-documenting-decisions`; the +``Notes`` section records the clause added. + Context ------- @@ -68,7 +71,7 @@ The glossary holds two kinds of term, and they impose different obligations. **Core terms are ours.** The package invented them and no service defines them: everything under *Retrieval*, *Failure and resumption*, *Configuration* -and *Boundaries*, plus *Collection family* and *Metadata*. One spelling, +and *Boundaries*, plus *Collection family*, *Metadata* and *Unified argument*. One spelling, enforced everywhere it appears -- prose, identifiers, tests. A second spelling of a core term is a defect, not a variation, and is fixed rather than recorded. This is what makes the lower-level modules shareable: transport, configuration @@ -99,6 +102,30 @@ Two rules follow: surface is not a legacy name; it is that adapter using its service's terms, and belongs with the term's own entry. +**A unified argument may stand beside a service's spelling.** Where one domain +concept is a filter in several adapters and each spells it differently, a getter +may also take an argument the package names -- ``state``, ``county`` -- that +accepts every common spelling of the concept and sends the service its own. +It is added to the adapter's surface and removes nothing from it, so a caller +who has read a service's documentation still finds the parameters described +there. It is allowed on four conditions: + +- **The service's parameters stay.** They are not deprecated, and they still + send the service's raw value, including values the conversion does not cover + (a non-US FIPS code, a county the service files under a retired name). +- **The conversion is exact.** It is a table in ``dataretrieval.codes``, checked + against the service's own reference collection by a live test. A value the + table does not hold raises ``ValueError`` naming the native parameters, + rather than being guessed at or sent unchecked. A service that cannot express + a converted value exactly raises too: NGWMN still files Connecticut sites + under the counties the state replaced with planning regions, so + ``ngwmn.get_sites(county="09110")`` raises rather than returning no rows. +- **It cannot be combined with what it replaces.** Passing it together with the + native parameter it is sent as raises ``ValueError``. +- **Its docstring names what it is sent as.** It says the argument is + dataretrieval's rather than the API's, and names the field or fields the + service receives, so the two documentations can be read against each other. + Consequences ------------ @@ -114,6 +141,10 @@ Consequences its definition rather than a list of exceptions. - A glossary entry now has an obligation to say which kind it is. That is a small cost per term and the reason the distinction is usable at all. +- A caller filtering by place across adapters learns one spelling rather than + one per service, at the cost of a table per concept to keep current. The + live tests that compare each table with its reference collection are what + keep that cost visible. - The package's own inconsistencies in core vocabulary become defects to fix rather than variations to tolerate. The resolution chain's ``tier``-for-*source* identifiers are the current example. @@ -135,6 +166,12 @@ reader notices immediately. Whether a given adapter docstring should say *site* or *monitoring location* remains a review judgement, and is meant to. +For unified arguments, ``tests/utils_test.py`` and ``tests/counties_test.py`` +compare the state and county tables with the Water Data ``states`` and +``counties`` collections, and ``tests/ngwmn_test.py`` checks that NGWMN files +its sites under the table's county names, live. Each getter's tests assert the +mutual exclusion with its native parameters. + Notes ----- @@ -145,3 +182,10 @@ made the gap visible rather than creating it. The per-service spellings in the table above were read from the adapters on 2026-09-01. + +The unified-argument clause was added on 2026-10-01, with ``county``. ``state`` +had been a unified argument since NGWMN joined the shared OGC code, with no +record saying why it did not contradict the rule that each adapter keeps its +service's spelling, and review read it as hiding the APIs' own parameters. The +clause states the conditions both arguments already met, so the next one is +judged against them rather than against that precedent. diff --git a/tests/counties_test.py b/tests/counties_test.py new file mode 100644 index 00000000..3d0f6885 --- /dev/null +++ b/tests/counties_test.py @@ -0,0 +1,161 @@ +"""``codes.counties``: the county table and the ``county`` argument's conversion. + +The getters that take ``county`` are tested in their adapter's file; this one +covers what they share -- which identifiers resolve, to what, and which raise. +""" + +import pytest + +from dataretrieval.codes.counties import apply_county, counties, to_county + + +@pytest.mark.parametrize( + ("value", "state"), + [ + ("55025", None), + (55025, None), + (" 55025 ", None), + ("US:55:025", None), + ("us:55:025", None), + ("025", "WI"), + ("Dane County", "WI"), + ("dane county", "Wisconsin"), + ("Dane", "55"), + ("DANE", "US:55"), + ("55025", "WI"), + ], +) +def test_every_form_resolves_to_the_same_county(value, state): + assert to_county(value, state=state) == "55025" + assert to_county(value, "fips_us", state=state) == "US:55:025" + assert to_county(value, "name", state=state) == "Dane County" + + +def test_an_integer_keeps_its_leading_zero(): + """Autauga County, Alabama is 01001; as an integer it is 1001.""" + assert to_county(1001) == "01001" + assert to_county("1001") == "01001" + + +@pytest.mark.parametrize( + ("value", "state", "name"), + [ + ("Orleans", "LA", "Orleans Parish"), + ("Juneau", "AK", "Juneau City and Borough"), + ("Bethel", "AK", "Bethel Census Area"), + ("Anchorage", "AK", "Anchorage Municipality"), + ("Adjuntas", "PR", "Adjuntas Municipio"), + ("Capitol", "CT", "Capitol Planning Region"), + ("St. Croix", "VI", "St. Croix Island"), + ("St Louis County", "MO", "St. Louis County"), + ("District of Columbia", "DC", "District of Columbia"), + ("Charles City", "VA", "Charles City County"), + ], +) +def test_a_county_equivalent_matches_without_its_designation(value, state, name): + """Parishes, boroughs, municipios, and planning regions resolve like + counties; periods are optional, as in "St Louis".""" + assert to_county(value, "name", state=state) == name + + +def test_a_list_resolves_element_wise(): + assert to_county(["55025", "US:17:031", 1001]) == ["55025", "17031", "01001"] + assert to_county(("Dane", "Iowa"), "name", state="WI") == [ + "Dane County", + "Iowa County", + ] + + +@pytest.mark.parametrize( + ("value", "state", "match"), + [ + ("Dane", None, r"names repeat across states\. Pass the state too"), + ("025", None, "not a recognized US county"), + ("99999", None, "not a recognized US county"), + (True, None, "not a recognized US county"), + ("55025", "IL", "is in Wisconsin, not Illinois"), + ("Fairfax", "VA", "'Fairfax County', 'Fairfax City'. Pass the full name"), + ("Baltimore", "MD", "more than one county in Maryland"), + ("Dain", "WI", "Did you mean 'Dane County'"), + ("Dane", ["WI", "IL"], "must be a single state"), + ("Dane", "Atlantis", "not a recognized US state"), + ], +) +def test_an_unresolvable_county_raises_with_a_remedy(value, state, match): + with pytest.raises(ValueError, match=match): + to_county(value, state=state) + + +def test_a_retired_connecticut_county_points_to_the_planning_regions(): + """Connecticut replaced its counties with planning regions in 2022; the + Water Data services accept only the new codes.""" + with pytest.raises(ValueError, match="planning regions, codes 09110 to 09190"): + to_county("09003") + + +def test_to_rejects_an_unknown_representation(): + with pytest.raises(ValueError, match="to"): + to_county("55025", "postal") + + +def test_the_table_covers_the_states_and_territories_and_only_them(): + from dataretrieval.codes.states import fips_codes + + assert {f[:2] for f in counties} == set(fips_codes.values()) + assert all(len(f) == 5 and f.isdigit() for f in counties) + assert not [f for f in counties if f.endswith("000")] + + +class TestApplyCounty: + @staticmethod + def _render(fips): + return {"county_code": fips} + + def test_absent_is_a_no_op_and_leaves_state_alone(self): + args = {"state": "WI"} + assert apply_county(args, render=self._render, reject=()) == {"state": "WI"} + + def test_state_qualifies_the_counties_and_is_consumed(self): + args = {"county": "Dane", "state": "WI", "limit": 5} + assert apply_county(args, render=self._render, reject=()) == { + "county_code": ["55025"], + "limit": 5, + } + + def test_a_native_parameter_cannot_be_combined_with_county(self): + with pytest.raises(ValueError, match="county and county_code cannot be"): + apply_county( + {"county": "55025", "county_code": "025"}, + render=self._render, + reject=("county_code",), + ) + + def test_an_unrecognized_county_names_the_native_parameters(self): + with pytest.raises(ValueError, match="Or pass state_code or county_code"): + apply_county( + {"county": "99999"}, + render=self._render, + reject=("state_code", "county_code"), + ) + + +@pytest.mark.live +def test_the_table_matches_the_water_data_counties_collection(): + """Each code and name is the one the services filter on. + + The table is a snapshot of this collection. A failure means the Census + Bureau or the service changed a county; regenerate the table from the + collection (``country_code=US``, without ``000`` and the states + ``codes.states`` does not cover). + """ + from dataretrieval.codes.states import fips_codes + from dataretrieval.waterdata import get_reference_table + + df, _ = get_reference_table("counties") + covered = set(fips_codes.values()) + live = { + row.state_fips_code + row.county_fips_code: row.county_name + for row in df[df["country_code"] == "US"].itertuples() + if row.state_fips_code in covered and row.county_fips_code != "000" + } + assert live == counties diff --git a/tests/ngwmn_test.py b/tests/ngwmn_test.py index d73ae0f2..26c4438a 100644 --- a/tests/ngwmn_test.py +++ b/tests/ngwmn_test.py @@ -11,6 +11,7 @@ cannot tell us, because the mock is what would need updating. """ +import json import re from urllib.parse import parse_qs, urlsplit @@ -348,6 +349,50 @@ def test_get_sites_state_accepts_name_postal_or_fips(httpx_mock): assert "state" not in qs +def test_get_sites_county_sends_state_name_and_county_name(httpx_mock): + """``sites`` files a county by name; ``state`` qualifies it.""" + _mock(httpx_mock, "sites", _SITES) + + ngwmn.get_sites(county="Dane", state="WI", skip_geometry=True) + ngwmn.get_sites(county="02020", skip_geometry=True) + + dane, anchorage = _queries(httpx_mock, "sites") + assert dane["state_name"] == ["Wisconsin"] + assert dane["county_name"] == ["Dane County"] + # NGWMN names Anchorage the other way round from the county table. + assert anchorage["county_name"] == ["Municipality of Anchorage"] + assert "county" not in dane and "state" not in dane + + +def test_get_sites_posts_a_county_name_list(httpx_mock): + """A comma-joined ``county_name`` matches nothing on NGWMN, where the + same names POSTed as CQL2 match; this applies to ``county_name`` too.""" + httpx_mock.add_response(method="POST", url=_items_re("sites"), json=_SITES) + + ngwmn.get_sites(county=["55025", "55065"], skip_geometry=True) + ngwmn.get_sites(county_name=["Dane County", "Lafayette County"], skip_geometry=True) + + for req in httpx_mock.get_requests(): + assert req.method == "POST" + (clause,) = json.loads(req.content)["args"] + assert clause["args"][1] == ["Dane County", "Lafayette County"] + first = parse_qs(urlsplit(str(httpx_mock.get_requests()[0].url)).query) + assert first["state_name"] == ["Wisconsin"] + + +@pytest.mark.parametrize( + ("kwargs", "match"), + [ + ({"county": ["55025", "17031"]}, "counties from one state per call"), + ({"county": "09110"}, "Pass county_name directly"), + ({"county": "55025", "county_name": "Dane County"}, "cannot be combined"), + ], +) +def test_get_sites_county_refusals(kwargs, match): + with pytest.raises(ValueError, match=match): + ngwmn.get_sites(**kwargs) + + # --- providers --------------------------------------------------------------- @@ -600,6 +645,32 @@ def test_a_configured_base_url_redirects_ngwmn_alone(httpx_mock): # --- live upstream monitor --------------------------------------------------- +@pytest.mark.live +def test_county_names_still_match_the_table_upstream(): + """Every county NGWMN files a site under is the table's name for it, but for + ``ngwmn._NGWMN_COUNTY_NAMES`` and the retired Connecticut counties. + + If this fails, NGWMN renamed a county (add it to ``_NGWMN_COUNTY_NAMES``), + or moved Connecticut to planning regions (drop ``_LEGACY_COUNTY_STATES``). + """ + from dataretrieval.codes.counties import counties + from dataretrieval.codes.states import to_state + from dataretrieval.ngwmn import _LEGACY_COUNTY_STATES, _NGWMN_COUNTY_NAMES + + df, _ = ngwmn.get_sites( + properties=["state_name", "county_name"], skip_geometry=True + ) + known = { + (to_state(f[:2], "name"), _NGWMN_COUNTY_NAMES.get(f, name)) + for f, name in counties.items() + if f[:2] not in _LEGACY_COUNTY_STATES + } + legacy = {to_state(s, "name") for s in _LEGACY_COUNTY_STATES} + filed = set(zip(df["state_name"], df["county_name"], strict=True)) + assert {p for p in filed if p[0] not in legacy} <= known + assert all(not name.endswith("Planning Region") for s, name in filed if s in legacy) + + @pytest.mark.live def test_state_queryables_still_diverge_upstream(): """The NGWMN ``sites`` and ``providers`` collections expose different state diff --git a/tests/nwdc_test.py b/tests/nwdc_test.py index d41fbb90..269af76c 100644 --- a/tests/nwdc_test.py +++ b/tests/nwdc_test.py @@ -402,6 +402,12 @@ def test_resolve_locations_requires_exactly_one(): with pytest.raises(ValueError, match="exactly one"): _resolve_locations(None, None, None) with pytest.raises(ValueError, match="exactly one"): + _resolve_locations(None, "55025", "04") + + +def test_state_qualifies_county_rather_than_naming_a_second_location(): + assert _resolve_locations("WI", "Dane", None) == ["countyCd:55025"] + with pytest.raises(ValueError, match="in Wisconsin, not Rhode Island"): _resolve_locations("RI", "55025", None) diff --git a/tests/utils_test.py b/tests/utils_test.py index c2fcb6c2..ef9a93e2 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -526,24 +526,31 @@ class TestTerritories: """ @pytest.mark.parametrize( - ("value", "name", "postal", "fips"), + ("name", "postal", "fips"), [ - ("Puerto Rico", "Puerto Rico", "PR", "72"), - ("PR", "Puerto Rico", "PR", "72"), - ("72", "Puerto Rico", "PR", "72"), - ("US:72", "Puerto Rico", "PR", "72"), - ("Guam", "Guam", "GU", "66"), - ("VI", "US Virgin Islands", "VI", "78"), - ("60", "American Samoa", "AS", "60"), - ("MP", "Northern Mariana Islands", "MP", "69"), + ("Puerto Rico", "PR", "72"), + ("Guam", "GU", "66"), + ("Virgin Islands", "VI", "78"), + ("American Samoa", "AS", "60"), + ("Northern Mariana Islands", "MP", "69"), ], ) - def test_every_encoding_resolves(self, value, name, postal, fips): + def test_each_territory_is_in_both_code_tables(self, name, postal, fips): + """Each code resolves to the others. The input encodings themselves are + :class:`Test_to_state`'s concern.""" from dataretrieval.codes.states import to_state - assert to_state(value, "name") == name - assert to_state(value, "postal") == postal - assert to_state(value, "fips") == fips + for value in (name, postal, fips): + assert to_state(value, "name") == name + assert to_state(value, "postal") == postal + assert to_state(value, "fips") == fips + + def test_the_census_name_of_the_virgin_islands_is_accepted(self): + """The table names it as the services do; the name it had before is + still accepted as input.""" + from dataretrieval.codes.states import to_state + + assert to_state("US Virgin Islands") == "Virgin Islands" def test_the_ngwmn_shim_routes_a_territory_to_each_queryable(self): """``sites`` filters on ``state_name``, ``providers`` on ``state``.""" @@ -557,6 +564,26 @@ def test_the_ngwmn_shim_routes_a_territory_to_each_queryable(self): } +@pytest.mark.live +def test_state_names_match_the_water_data_states_collection(): + """Each name is the one the services filter ``state_name`` on (ADR 0013). + + Water Data and NGWMN match ``state_name`` exactly, so a name that differs + from theirs returns no rows rather than an error. + """ + from dataretrieval.codes.states import fips_codes, state_codes + from dataretrieval.waterdata import get_reference_table + + df, _ = get_reference_table("states") + expected = {fips: (name, state_codes[name]) for name, fips in fips_codes.items()} + live = { + row.state_fips_code: (row.state_name, row.state_postal_code.lower()) + for row in df[df["country_code"] == "US"].itertuples() + if row.state_fips_code in expected + } + assert live == expected + + class TestApplyStateUnrecognized: """The remedy for an unknown value names the endpoint's API state parameter. diff --git a/tests/waterdata_test.py b/tests/waterdata_test.py index dbde43cf..d5051192 100644 --- a/tests/waterdata_test.py +++ b/tests/waterdata_test.py @@ -976,6 +976,72 @@ def test_get_monitoring_locations(httpx_mock): assert hasattr(md, "url") and hasattr(md, "query_time") +def test_county_is_sent_as_state_code_and_county_code_pairs(httpx_mock): + """A county code repeats across states, so ``county`` sends the pair; with + ``state``, the state qualifies the county rather than filtering again.""" + _mock_items(httpx_mock, "monitoring-locations") + _mock_items(httpx_mock, "combined-metadata") + + get_monitoring_locations(county="Dane", state="WI") + get_combined_metadata(county=55025) + + for qs in _sent(httpx_mock): + assert qs["state_code"] == ["55"] + assert qs["county_code"] == ["025"] + assert "state_name" not in qs and "state" not in qs and "county" not in qs + + +def test_counties_in_one_state_post_a_county_code_list(httpx_mock): + _mock_items(httpx_mock, "monitoring-locations") + + get_monitoring_locations(county=["55025", "US:55:021"]) + + (req,) = httpx_mock.get_requests() + assert req.method == "POST" + assert parse_qs(urlsplit(str(req.url)).query)["state_code"] == ["55"] + assert json.loads(req.content)["args"] == [ + {"op": "in", "args": [{"property": "county_code"}, ["025", "021"]]} + ] + + +def test_counties_in_several_states_or_their_pairs_in_a_filter(httpx_mock): + """Separate state_code and county_code lists would also match Cook + County's code (031) in Wisconsin; the pairs must be OR-ed.""" + _mock_items(httpx_mock, "monitoring-locations") + + get_monitoring_locations(county=["55025", "17031"]) + + (qs,) = _sent(httpx_mock) + assert qs["filter"] == [ + "(state_code='55' AND county_code='025') OR " + "(state_code='17' AND county_code='031')" + ] + assert "state_code" not in qs and "county_code" not in qs + + +def test_counties_in_several_states_cannot_join_a_caller_filter(): + with pytest.raises(ValueError, match="county and filter cannot be combined"): + get_monitoring_locations( + county=["55025", "17031"], filter="site_type_code='ST'" + ) + + +def test_county_in_one_state_keeps_a_caller_filter(httpx_mock): + _mock_items(httpx_mock, "monitoring-locations") + + get_monitoring_locations(county="55025", filter="site_type_code='ST'") + + (qs,) = _sent(httpx_mock) + assert qs["filter"] == ["site_type_code='ST'"] + assert qs["county_code"] == ["025"] + + +@pytest.mark.parametrize("native", ["state_code", "state_name", "county_code"]) +def test_county_cannot_be_combined_with_a_native_location_parameter(native): + with pytest.raises(ValueError, match=f"county and {native} cannot be combined"): + get_monitoring_locations(county="55025", **{native: "x"}) + + def test_get_monitoring_locations_hucs_uses_post_cql(httpx_mock): """``monitoring-locations`` is a POST/CQL2 collection in the Water Data dialect, so a multi-value filter goes out as a CQL2 body rather than a @@ -1452,6 +1518,24 @@ def test_get_stats_por(httpx_mock): assert df.loc[df["computation"] == "arithmetic_mean", "percentile"].isna().all() +def test_stats_county_is_sent_in_the_us_state_county_form(httpx_mock): + _mock_stats(httpx_mock, "observationIntervals") + + get_stats_date_range(county=["Dane", "025", 55021], state="WI") + + (req,) = httpx_mock.get_requests() + qs = parse_qs(urlsplit(str(req.url)).query) + # Deduplicated, in order; state named the counties' state, so no state_code. + assert qs["county_code"] == ["US:55:025", "US:55:021"] + assert "state_code" not in qs + + +@pytest.mark.parametrize("native", ["state_code", "county_code"]) +def test_stats_county_cannot_be_combined_with_a_native_location_parameter(native): + with pytest.raises(ValueError, match=f"county and {native} cannot be combined"): + get_stats_por(county="55025", **{native: "US:55"}) + + def test_get_stats_por_expanded_false(httpx_mock): """``expand_percentiles=False`` keeps the raw ``percentiles`` list column instead of expanding it into one row per percentile."""