From e3c3c335d2b1c9c7aabeb9715b3bea22808a30bf Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Thu, 1 Oct 2026 15:50:04 -0500 Subject: [PATCH 1/5] fix(codes): name the Virgin Islands as the services do codes.states named FIPS 78 "US Virgin Islands". Water Data and NGWMN both filter state_name on "Virgin Islands" and match exactly, so state="VI" returned no rows from either service: 0 rather than 1,092 monitoring locations, and 0 rather than 3 NGWMN sites. The Census name is still accepted as input. The territory tests are consolidated to one row per territory, each checking that its name, postal code, and FIPS code resolve to one another; the encodings themselves (case, US: prefix) are Test_to_state's. --- dataretrieval/codes/states.py | 7 +++++-- tests/utils_test.py | 33 ++++++++++++++++++++------------- 2 files changed, 25 insertions(+), 15 deletions(-) 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/tests/utils_test.py b/tests/utils_test.py index c2fcb6c2..89c7d015 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``.""" From a78a92443f5e0829dc434c745455fe7387c717cf Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Thu, 1 Oct 2026 16:17:12 -0500 Subject: [PATCH 2/5] feat: add a unified county argument, like state county accepts a five-digit FIPS code, the statistics service's US:SS:CCC, or a name or three-digit code with its state, and sends each service what it filters on: Water Data state_code + county_code, statistics county_code=US:SS:CCC, NGWMN state_name + county_name, NWDC countyCd. With county, state qualifies the counties instead of filtering on its own. Scoped as state is: the location and statistics getters, ngwmn.get_sites, and nwdc.get_wateruse; not WQP, NWIS, or Samples, which have no state argument either, and not get_time_series_metadata or ngwmn.get_providers, which have no county field. The table is the Water Data counties collection (3,239 counties in the states and territories codes.states covers), in its own data module so the doubled-word hook can skip it ("Walla Walla County"). Every entry round-trips through every input form, and a live test compares the table with the collection. A county code repeats across states, so counties in several states go to the Water Data location collections as a cql-text filter OR-ing (state_code, county_code) pairs, which the planner splits at top-level OR: all 174 Wisconsin and Illinois counties returned the same 5,136 stream sites as state=[WI, IL], in 2 requests. NGWMN's edge refuses cql-text filters (403), so get_sites takes one state per call. NGWMN names Anchorage differently and still files Connecticut sites under the counties the state replaced with planning regions in 2022; the first is aliased, the second raises with a county_name remedy, and a live test fails if either changes. Fixes ngwmn.get_sites(county_name=[...]) returning no rows: NGWMN matches nothing for a comma-joined county_name, so multi-value sites filters are now POSTed as CQL2 (checked live for state_name, monitoring_location_id, agency_code, and county_name). --- .pre-commit-config.yaml | 2 +- NEWS.md | 2 + dataretrieval/codes/__init__.py | 8 +- dataretrieval/codes/_county_table.py | 3253 ++++++++++++++++++++++++ dataretrieval/codes/counties.py | 260 ++ dataretrieval/ngwmn.py | 68 +- dataretrieval/nwdc.py | 41 +- dataretrieval/waterdata/metadata.py | 31 +- dataretrieval/waterdata/time_series.py | 23 +- dataretrieval/waterdata/utils.py | 58 + tests/counties_test.py | 161 ++ tests/ngwmn_test.py | 71 + tests/nwdc_test.py | 6 + tests/waterdata_test.py | 84 + 14 files changed, 4033 insertions(+), 35 deletions(-) create mode 100644 dataretrieval/codes/_county_table.py create mode 100644 dataretrieval/codes/counties.py create mode 100644 tests/counties_test.py 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/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..9551a42e --- /dev/null +++ b/dataretrieval/codes/counties.py @@ -0,0 +1,260 @@ +"""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 and passed to + ``render``, 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(fips if isinstance(fips, list) else [fips])) + return local_vars + + +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, state_fips) + 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, state_fips) + 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, state_fips: str | None) -> ValueError: + """The error for a code that names no county in the table.""" + hint = "" + if str(value).strip().removeprefix("US:").startswith("09") or state_fips == "09": + hint = ( + " Connecticut's counties were replaced in 2022 by planning " + "regions, codes 09110 to 09190." + ) + 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/ngwmn.py b/dataretrieval/ngwmn.py index ced8cc79..979159e1 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,52 @@ # (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.""" + fips = list(dict.fromkeys(fips)) + states = list(dict.fromkeys(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}." + ) + names = [_NGWMN_COUNTY_NAMES.get(f, counties[f]) for f in fips] + return { + "state_name": to_state(states[0], "name"), + "county_name": names[0] if len(names) == 1 else names, + } + + +# 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 +145,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 +175,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 +223,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..60196b36 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 statistics service's form + (``"US:55:025"``), 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 statistics service's form + (``"US:55:025"``), 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..0e0ca4a1 100644 --- a/dataretrieval/waterdata/utils.py +++ b/dataretrieval/waterdata/utils.py @@ -22,6 +22,7 @@ import pandas as pd from dataretrieval._deprecation import warn_deprecated +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 +190,63 @@ 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``.""" + fips = list(dict.fromkeys(fips)) + states = list(dict.fromkeys(f[:2] for f in fips)) + if len(states) == 1: + codes = [f[2:] for f in fips] + return { + "state_code": states[0], + "county_code": codes[0] if len(codes) == 1 else codes, + } + if user_filter is not None: + raise ValueError( + "county names counties in more than one state, which is sent " + "as a filter, so it cannot be combined with filter. 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 dict.fromkeys(fips)] + }, + reject=("state_code", "county_code"), + ) + + def get_ogc_data( args: dict[str, Any], collection: str, 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/waterdata_test.py b/tests/waterdata_test.py index dbde43cf..8eb9a4ea 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="cannot be combined with filter"): + 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.""" From 02e48581cdba711689fb2e57fea239d9fd13efb5 Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Thu, 1 Oct 2026 16:17:24 -0500 Subject: [PATCH 3/5] docs: record unified arguments in the glossary and ADR 0013 state and now county are package-named arguments for domain concepts that ADR 0013 otherwise leaves to each service's spelling. Review of #422 read state as hiding the APIs' own parameters, and nothing recorded why it did not contradict the rule. ADR 0013 gains a clause stating the four conditions both meet: the native parameters stay, the conversion is an exact table kept against the service's reference collection, combining the two raises, and the docstring names what the argument is sent as. CONTEXT.md defines State and County as domain terms with each service's spelling, and Unified argument as a core term. --- CONTEXT.md | 30 ++++++++++++ .../decisions/0013-core-and-domain-terms.rst | 46 ++++++++++++++++++- 2 files changed, 75 insertions(+), 1 deletion(-) 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/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. From 39d31439a1f9aa257d2bf0db45b31a42915ca68a Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Thu, 1 Oct 2026 19:22:19 -0500 Subject: [PATCH 4/5] refactor(county): deduplicate once in apply_county; trim render shapes Simplify review of #433: - apply_county deduplicates the counties before calling render, so the three renderers no longer each repeat list(dict.fromkeys(...)). - Renderers return lists unconditionally. The single-value collapse was unneeded: the request builder already sends a one-element list as a plain GET parameter (checked live: same URLs and rows). - The multi-state-with-filter conflict raises through _validation.reject_together, in the package's shared wording. - The Connecticut hint is keyed on the parsed code rather than a second, looser parse of the raw value. - The location getters' county docstrings name the US:55:025 form as the others do. --- dataretrieval/codes/counties.py | 32 +++++++++++++++++------------ dataretrieval/ngwmn.py | 6 ++---- dataretrieval/waterdata/metadata.py | 8 ++++---- dataretrieval/waterdata/utils.py | 30 +++++++++++---------------- tests/waterdata_test.py | 2 +- 5 files changed, 38 insertions(+), 40 deletions(-) diff --git a/dataretrieval/codes/counties.py b/dataretrieval/codes/counties.py index 9551a42e..96d22463 100644 --- a/dataretrieval/codes/counties.py +++ b/dataretrieval/codes/counties.py @@ -101,9 +101,10 @@ def apply_county( 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 and passed to - ``render``, which returns the parameters this service filters on; they are - merged into ``local_vars``, which is returned. + 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 @@ -126,10 +127,15 @@ def apply_county( f"{err} Or pass {' or '.join(reject)} directly, using the API's " "native value." ) from err - local_vars.update(render(fips if isinstance(fips, list) else [fips])) + 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)): @@ -147,7 +153,7 @@ def _single_state_fips(state: object) -> str: 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, state_fips) + raise _unrecognized(value) s = str(value).strip() fips = _parse_code(s, state_fips) if fips is None: @@ -159,7 +165,7 @@ def _to_fips_one(value: str | int, state_fips: str | None) -> str: ) fips = _match_name(s, state_fips) if fips not in counties: - raise _unrecognized(value, state_fips) + 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 " @@ -236,14 +242,14 @@ def _suggest(wanted: str, names: Iterable[str]) -> str: return f" Did you mean {', '.join(suggestions)}?" if suggestions else "" -def _unrecognized(value: object, state_fips: str | None) -> ValueError: +def _unrecognized(value: object, *, in_connecticut: bool = False) -> ValueError: """The error for a code that names no county in the table.""" - hint = "" - if str(value).strip().removeprefix("US:").startswith("09") or state_fips == "09": - hint = ( - " Connecticut's counties were replaced in 2022 by planning " - "regions, codes 09110 to 09190." - ) + 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 " diff --git a/dataretrieval/ngwmn.py b/dataretrieval/ngwmn.py index 979159e1..86bc5f75 100644 --- a/dataretrieval/ngwmn.py +++ b/dataretrieval/ngwmn.py @@ -95,8 +95,7 @@ def _sites_county_params(fips: list[str]) -> dict[str, Any]: """``state_name`` plus ``county_name`` for counties in one state.""" - fips = list(dict.fromkeys(fips)) - states = list(dict.fromkeys(f[:2] for f in fips)) + 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 @@ -113,10 +112,9 @@ def _sites_county_params(fips: list[str]) -> dict[str, Any]: "matches. Pass county_name directly, e.g. county_name='Hartford " f"County', state={to_state(states[0], 'postal')!r}." ) - names = [_NGWMN_COUNTY_NAMES.get(f, counties[f]) for f in fips] return { "state_name": to_state(states[0], "name"), - "county_name": names[0] if len(names) == 1 else names, + "county_name": [_NGWMN_COUNTY_NAMES.get(f, counties[f]) for f in fips], } diff --git a/dataretrieval/waterdata/metadata.py b/dataretrieval/waterdata/metadata.py index 60196b36..5acdb93c 100644 --- a/dataretrieval/waterdata/metadata.py +++ b/dataretrieval/waterdata/metadata.py @@ -137,8 +137,8 @@ def get_monitoring_locations( 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 statistics service's form - (``"US:55:025"``), or, with ``state``, a name (``"Dane County"`` or + 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 @@ -750,8 +750,8 @@ def get_combined_metadata( 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 statistics service's form - (``"US:55:025"``), or, with ``state``, a name (``"Dane County"`` or + 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 diff --git a/dataretrieval/waterdata/utils.py b/dataretrieval/waterdata/utils.py index 0e0ca4a1..49df745d 100644 --- a/dataretrieval/waterdata/utils.py +++ b/dataretrieval/waterdata/utils.py @@ -22,6 +22,7 @@ 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 @@ -211,22 +212,17 @@ def _with_location_county(local_vars: dict[str, Any]) -> dict[str, Any]: def _location_county_params(fips: list[str], user_filter: object) -> dict[str, Any]: """The location collections' parameters for counties ``fips``.""" - fips = list(dict.fromkeys(fips)) - states = list(dict.fromkeys(f[:2] for f in fips)) + states = {f[:2] for f in fips} if len(states) == 1: - codes = [f[2:] for f in fips] - return { - "state_code": states[0], - "county_code": codes[0] if len(codes) == 1 else codes, - } - if user_filter is not None: - raise ValueError( - "county names counties in more than one state, which is sent " - "as a filter, so it cannot be combined with filter. 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 {"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 @@ -240,9 +236,7 @@ def _with_statistics_county(local_vars: dict[str, Any]) -> dict[str, Any]: _flatten_queryables(local_vars) return apply_county( local_vars, - render=lambda fips: { - "county_code": [f"US:{f[:2]}:{f[2:]}" for f in dict.fromkeys(fips)] - }, + render=lambda fips: {"county_code": [f"US:{f[:2]}:{f[2:]}" for f in fips]}, reject=("state_code", "county_code"), ) diff --git a/tests/waterdata_test.py b/tests/waterdata_test.py index 8eb9a4ea..d5051192 100644 --- a/tests/waterdata_test.py +++ b/tests/waterdata_test.py @@ -1020,7 +1020,7 @@ def test_counties_in_several_states_or_their_pairs_in_a_filter(httpx_mock): def test_counties_in_several_states_cannot_join_a_caller_filter(): - with pytest.raises(ValueError, match="cannot be combined with filter"): + with pytest.raises(ValueError, match="county and filter cannot be combined"): get_monitoring_locations( county=["55025", "17031"], filter="site_type_code='ST'" ) From c77c9b670dfcc838caf6a2545baeeec782025d13 Mon Sep 17 00:00:00 2001 From: thodson-usgs Date: Thu, 1 Oct 2026 20:09:46 -0500 Subject: [PATCH 5/5] test: check the state table against the Water Data states collection ADR 0013's unified-argument clause requires each conversion table to be checked live against the service's reference collection. The county table is (counties_test.py); this does the same for codes.states, whose names Water Data and NGWMN match exactly. It would have caught the Virgin Islands name fixed in #432. --- tests/utils_test.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/utils_test.py b/tests/utils_test.py index 89c7d015..ef9a93e2 100644 --- a/tests/utils_test.py +++ b/tests/utils_test.py @@ -564,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.