diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..3e8f6fe2 --- /dev/null +++ b/.coveragerc @@ -0,0 +1,29 @@ +[run] +branch = True + +[report] +# Regexes for lines to exclude from consideration +exclude_lines = + # Have to re-enable the standard pragma + pragma: no cover + + # Don't complain about missing debug-only code: + def __repr__ + if self\.debug + + # Don't complain if tests don't hit defensive assertion code: + raise AssertionError + raise NotImplementedError + + # Don't complain if non-runnable code isn't run: + if __name__ == .__main__.: + + # Don't complain about abstract methods, they aren't run: + @(abc\.)?abstractmethod + + +omit = + # omit test files + tests/* + # omit setup file + setup.py diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml index 11439c77..9e89968a 100644 --- a/.github/workflows/pytest.yml +++ b/.github/workflows/pytest.yml @@ -9,12 +9,12 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest, windows-latest] - python-version: ['3.10'] + python-version: [3.11] include: - os: ubuntu-latest - python-version: 3.9 + python-version: '3.10' - os: ubuntu-latest - python-version: 3.8 + python-version: 3.9 steps: - uses: actions/checkout@v2 @@ -34,14 +34,4 @@ jobs: python -m pip install . - name: Run pytest and Generate coverage report run: | - python -m pytest -v --disable-warnings --cov=./ --cov-report=xml:coverage.xml - - name: Upload coverage to Codecov - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} - file: ./coverage.xml - flags: unittests - env_vars: OS,PYTHON - name: codecov-umbrella - fail_ci_if_error: false - verbose: true + python -m pytest --ignore=tests/ords --ignore=tests/utilities --ignore=tests/web -v --disable-warnings diff --git a/.github/workflows/pytest_ords.yml b/.github/workflows/pytest_ords.yml new file mode 100644 index 00000000..cc097142 --- /dev/null +++ b/.github/workflows/pytest_ords.yml @@ -0,0 +1,49 @@ +name: pytests-ords + +on: pull_request + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + python-version: [3.11] + + steps: + - uses: actions/checkout@v2 + with: + ref: ${{ github.event.pull_request.head.ref }} + fetch-depth: 1 + - name: Set up Python ${{ matrix.python-version }} + uses: conda-incubator/setup-miniconda@v2 + with: + auto-update-conda: true + python-version: ${{ matrix.python-version }} + miniconda-version: "latest" + - name: Install dependencies' + shell: bash -l {0} + run: | + conda install -c conda-forge poppler + python -m pip install --upgrade pip + python -m pip install pdftotext + python -m pip install pytest + python -m pip install pytest-mock + python -m pip install pytest-cov + python -m pip install . + playwright install + - name: Run pytest and Generate coverage report + shell: bash -l {0} + run: | + python -m pytest -v --disable-warnings --cov=./ --cov-report=xml:coverage.xml + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: ./coverage.xml + flags: unittests + env_vars: OS,PYTHON + name: codecov-umbrella + fail_ci_if_error: false + verbose: true diff --git a/README.rst b/README.rst index f26be1f9..542bd413 100644 --- a/README.rst +++ b/README.rst @@ -33,6 +33,9 @@ Installing ELM .. inclusion-install +NOTE: If you are installing ELM to run ordinance scraping and extraction, +see the `ordinance-specific installation instructions `_. + Option #1 (basic usage): #. ``pip install NREL-elm`` diff --git a/docs/source/_cli/cli.rst b/docs/source/_cli/cli.rst new file mode 100644 index 00000000..a566d657 --- /dev/null +++ b/docs/source/_cli/cli.rst @@ -0,0 +1,8 @@ +.. _cli-docs: + +Command Line Interfaces (CLIs) +============================== + +.. toctree:: + + elm diff --git a/docs/source/_cli/elm.rst b/docs/source/_cli/elm.rst new file mode 100644 index 00000000..aa55997f --- /dev/null +++ b/docs/source/_cli/elm.rst @@ -0,0 +1,3 @@ +.. click:: elm.cli:main + :prog: elm + :nested: full \ No newline at end of file diff --git a/docs/source/examples.ordinance_gpt.rst b/docs/source/examples.ordinance_gpt.rst new file mode 100644 index 00000000..2cb4845c --- /dev/null +++ b/docs/source/examples.ordinance_gpt.rst @@ -0,0 +1,2 @@ +.. include:: ../../examples/ordinance_gpt/README.rst + :start-line: 0 diff --git a/docs/source/examples.rst b/docs/source/examples.rst index b552d164..84582d5e 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -3,3 +3,4 @@ Examples .. toctree:: examples.energy_wizard.rst + examples.ordinance_gpt.rst diff --git a/docs/source/index.rst b/docs/source/index.rst index 1a569a7c..998fc741 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -5,5 +5,6 @@ Installation Examples API reference <_autosummary/elm> + CLI reference <_cli/cli> .. include:: ../../README.rst diff --git a/elm/cli.py b/elm/cli.py new file mode 100644 index 00000000..d2594b18 --- /dev/null +++ b/elm/cli.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +# fmt: off +"""ELM Ordinances CLI.""" +import sys +import json +import click +import asyncio +import logging + +from elm.version import __version__ +from elm.ords.process import process_counties_with_openai + + +@click.group() +@click.version_option(version=__version__) +@click.pass_context +def main(ctx): + """ELM ordinances command line interface.""" + ctx.ensure_object(dict) + + +@main.command() +@click.option("--config", "-c", required=True, type=click.Path(exists=True), + help="Path to ordinance configuration JSON file. This file " + "should contain any/all the arguments to pass to " + ":func:`elm.ords.process.process_counties_with_openai`.") +@click.option("-v", "--verbose", is_flag=True, + help="Flag to show logging on the terminal. Default is not " + "to show any logs on the terminal.") +def ords(config, verbose): + """Download and extract ordinances for a list of counties.""" + with open(config, "r") as fh: + config = json.load(fh) + + if verbose: + logger = logging.getLogger("elm") + logger.addHandler(logging.StreamHandler(stream=sys.stdout)) + logger.setLevel(config.get("log_level", "INFO")) + + # asyncio.run(...) doesn't throw exceptions correctly for some reason... + loop = asyncio.get_event_loop() + loop.run_until_complete(process_counties_with_openai(**config)) + + +if __name__ == "__main__": + # pylint: disable=no-value-for-parameter + main(obj={}) diff --git a/elm/exceptions.py b/elm/exceptions.py new file mode 100644 index 00000000..3a2e819d --- /dev/null +++ b/elm/exceptions.py @@ -0,0 +1,10 @@ +# -*- coding: utf-8 -*- +"""Custom Exceptions and Errors for ELM. """ + + +class ELMError(Exception): + """Generic ELM Error.""" + + +class ELMRuntimeError(ELMError, RuntimeError): + """ELM RuntimeError.""" diff --git a/elm/ords/README.md b/elm/ords/README.md new file mode 100644 index 00000000..46678e3b --- /dev/null +++ b/elm/ords/README.md @@ -0,0 +1,27 @@ +# Welcome to Energy Language Model - OrdinanceGPT + +The ordinance web scraping and data extraction portion of this codebase required a few extra dependencies that do not come out-of-the-box with the base ELM software. +To set up ELM for ordinances, first create a conda environment. Then, _before installing ELM_, run the poppler installation: + + $ conda install -c conda-forge poppler + +Then, install `pdftotext`: + + $ pip install pdftotext + +(OPTIONAL) If you want to have access to Optical Character Recognition (OCR) for PDF parsing, you should also install pytesseract during this step: + + $ pip install pytesseract pdf2image + +At this point, you can install ELM per the [front-page README](https://github.com/NREL/elm/blob/main/README.rst) instructions, e.g.: + + $ pip install -e . + +After ELM installs successfully, you must instantiate the playwright module, which is used for web scraping. +To do so, simply run: + + $ playwright install + +Now you are ready to run ordinance retrieval and extraction. See the [example](https://github.com/NREL/elm/blob/main/examples/ordinance_gpt/README.rst) to get started. If you get additional import errors, just install additional packages as necessary, e.g.: + + $ pip install beautifulsoup4 html5lib diff --git a/elm/ords/__init__.py b/elm/ords/__init__.py new file mode 100644 index 00000000..b3df756c --- /dev/null +++ b/elm/ords/__init__.py @@ -0,0 +1 @@ +"""ELM ordinance document download and structured data extraction. """ diff --git a/elm/ords/data/conus_counties.csv b/elm/ords/data/conus_counties.csv new file mode 100644 index 00000000..c58efb6a --- /dev/null +++ b/elm/ords/data/conus_counties.csv @@ -0,0 +1,3110 @@ +County,State,County Code,State Code,FIPS,County Type,Full Name,Website +Abbeville,South Carolina,1,45,45001,county,Abbeville County,https://abbevillecountysc.com/ +Acadia,Louisiana,1,22,22001,parish,Acadia Parish,https://www.louisiana.gov/local-louisiana/acadia-parish +Accomack,Virginia,1,51,51001,county,Accomack County,https://www.co.accomack.va.us/ +Ada,Idaho,1,16,16001,county,Ada County,https://adacounty.id.gov/ +Adair,Iowa,1,19,19001,county,Adair County,https://www.adaircounty.iowa.gov/ +Adair,Kentucky,1,21,21001,county,Adair County,https://adaircounty.ky.gov/Pages/index.aspx +Adair,Missouri,1,29,29001,county,Adair County,https://adaircountymissouri.com/ +Adair,Oklahoma,1,40,40001,county,Adair County,https://oklahoma.gov/okdhs/library/resources/adairrd211.html +Adams,Indiana,1,18,18001,county,Adams County,https://www.co.adams.in.us/ +Adams,Idaho,3,16,16003,county,Adams County,https://www.co.adams.id.us/ +Adams,Nebraska,1,31,31001,county,Adams County,https://www.adamscounty.org/ +Adams,Iowa,3,19,19003,county,Adams County,https://adamscounty.iowa.gov/ +Adams,Ohio,1,39,39001,county,Adams County,https://adamscountyoh.gov/ +Adams,Wisconsin,1,55,55001,county,Adams County,https://www.co.adams.wi.us/ +Adams,Colorado,1,8,8001,county,Adams County,https://adcogov.org/ +Adams,Illinois,1,17,17001,county,Adams County,https://www.co.adams.il.us/ +Adams,Mississippi,1,28,28001,county,Adams County,https://www.adamscountyms.net/ +Adams,Washington,1,53,53001,county,Adams County,https://www.co.adams.wa.us/ +Adams,North Dakota,1,38,38001,county,Adams County,https://www.adamscountynd.com/ +Adams,Pennsylvania,1,42,42001,county,Adams County,https://www.adamscountypa.gov/ +Addison,Vermont,1,50,50001,county,Addison County,https://prosecutors.vermont.gov/offices/addison/ +Aiken,South Carolina,3,45,45003,county,Aiken County,https://www.aikencountysc.gov/ +Aitkin,Minnesota,1,27,27001,county,Aitkin County,https://www.co.aitkin.mn.us/ +Alachua,Florida,1,12,12001,county,Alachua County,https://www.alachuacounty.us/ +Alamance,North Carolina,1,37,37001,county,Alamance County,https://www.alamance-nc.com/ +Alameda,California,1,6,6001,county,Alameda County,https://www.acgov.org/ +Alamosa,Colorado,3,8,8003,county,Alamosa County,https://alamosacounty.colorado.gov/ +Albany,Wyoming,1,56,56001,county,Albany County,https://www.co.albany.wy.us/ +Albany,New York,1,36,36001,county,Albany County,https://www.albanycounty.com/ +Albemarle,Virginia,3,51,51003,county,Albemarle County,https://www.albemarle.org/ +Alcona,Michigan,1,26,26001,county,Alcona County,https://alconacountymi.com/wp/ +Alcorn,Mississippi,3,28,28003,county,Alcorn County,http://alcorncounty.org/ +Alexander,North Carolina,3,37,37003,county,Alexander County,https://alexandercountync.gov/ +Alexander,Illinois,3,17,17003,county,Alexander County,https://alexandercounty.illinois.gov/ +Alexandria,Virginia,510,51,51510,city,Alexandria city,https://www.alexandriava.gov/ +Alfalfa,Oklahoma,3,40,40003,county,Alfalfa County,https://alfalfa.okcounties.org/ +Alger,Michigan,3,26,26003,county,Alger County,https://www.algercounty.gov/ +Allamakee,Iowa,5,19,19005,county,Allamakee County,https://allamakeecounty.iowa.gov/ +Allegan,Michigan,5,26,26005,county,Allegan County,https://www.allegancounty.org/ +Allegany,New York,3,36,36003,county,Allegany County,https://www.alleganyco.gov/ +Allegany,Maryland,1,24,24001,county,Allegany County,https://www.gov.allconet.org/ +Alleghany,Virginia,5,51,51005,county,Alleghany County,https://www.co.alleghany.va.us/ +Alleghany,North Carolina,5,37,37005,county,Alleghany County,https://alleghanycounty-nc.gov/ +Allegheny,Pennsylvania,3,42,42003,county,Allegheny County,https://www.alleghenycounty.us/ +Allen,Ohio,3,39,39003,county,Allen County,https://www.allencountyohio.com/ +Allen,Kansas,1,20,20001,county,Allen County,https://www.allencounty.org/ +Allen,Indiana,3,18,18003,county,Allen County,https://www.allencounty.in.gov/ +Allen,Louisiana,3,22,22003,parish,Allen Parish,https://www.allenparish.com/ +Allen,Kentucky,3,21,21003,county,Allen County,https://www.allencountykentucky.com/ +Allendale,South Carolina,5,45,45005,county,Allendale County,https://www.allendalecounty.com/ +Alpena,Michigan,7,26,26007,county,Alpena County,https://www.alpenacounty.org/ +Alpine,California,3,6,6003,county,Alpine County,https://www.alpinecountyca.gov/ +Amador,California,5,6,6005,county,Amador County,https://www.amadorgov.org/ +Amelia,Virginia,7,51,51007,county,Amelia County,https://www.ameliacova.com/ +Amherst,Virginia,9,51,51009,county,Amherst County,https://www.countyofamherst.com/ +Amite,Mississippi,5,28,28005,county,Amite County,http://www.amitecounty.ms/ +Anderson,Kansas,3,20,20003,county,Anderson County,http://andersoncountyks.org/ +Anderson,Texas,1,48,48001,county,Anderson County,https://www.co.anderson.tx.us/ +Anderson,South Carolina,7,45,45007,county,Anderson County,https://www.andersoncountysc.org/ +Anderson,Kentucky,5,21,21005,county,Anderson County,https://andersoncounty.ky.gov/departments/Pages/default.aspx +Anderson,Tennessee,1,47,47001,county,Anderson County,https://andersoncountytn.gov/ +Andrew,Missouri,3,29,29003,county,Andrew County,https://andrewcounty.org/ +Andrews,Texas,3,48,48003,county,Andrews County,https://www.co.andrews.tx.us/ +Androscoggin,Maine,1,23,23001,county,Androscoggin County,https://www.androscoggincountymaine.gov/ +Angelina,Texas,5,48,48005,county,Angelina County,https://www.angelinacounty.net/ +Anne Arundel,Maryland,3,24,24003,county,Anne Arundel County,https://www.aacounty.org/home +Anoka,Minnesota,3,27,27003,county,Anoka County,https://www.anokacountymn.gov/ +Anson,North Carolina,7,37,37007,county,Anson County,https://www.co.anson.nc.us/ +Antelope,Nebraska,3,31,31003,county,Antelope County,https://antelopecounty.nebraska.gov/ +Antrim,Michigan,9,26,26009,county,Antrim County,https://www.antrimcounty.org/ +Apache,Arizona,1,4,4001,county,Apache County,https://www.apachecountyaz.gov/ +Appanoose,Iowa,7,19,19007,county,Appanoose County,https://appanoosecounty.iowa.gov/ +Appling,Georgia,1,13,13001,county,Appling County,https://applingcountyga.org/ +Appomattox,Virginia,11,51,51011,county,Appomattox County,https://www.appomattoxcountyva.gov/ +Aransas,Texas,7,48,48007,county,Aransas County,https://www.aransascountytx.gov/main/ +Arapahoe,Colorado,5,8,8005,county,Arapahoe County,https://www.arapahoeco.gov/ +Archer,Texas,9,48,48009,county,Archer County,https://www.co.archer.tx.us/ +Archuleta,Colorado,7,8,8007,county,Archuleta County,https://www.archuletacounty.org/ +Arenac,Michigan,11,26,26011,county,Arenac County,https://www.arenaccountymi.gov/ +Arkansas,Arkansas,1,5,5001,county,Arkansas County,https://local.arkansas.gov/local.php?agency=Arkansas%20County +Arlington,Virginia,13,51,51013,county,Arlington County,https://www.arlingtonva.us/Home +Armstrong,Pennsylvania,5,42,42005,county,Armstrong County,https://co.armstrong.pa.us/ +Armstrong,Texas,11,48,48011,county,Armstrong County,https://www.co.armstrong.tx.us/ +Aroostook,Maine,3,23,23003,county,Aroostook County,https://aroostook.me.us/ +Arthur,Nebraska,5,31,31005,county,Arthur County,https://arthurcounty.nebraska.gov/content/home +Ascension,Louisiana,5,22,22005,parish,Ascension Parish,http://www.ascensionparish.net/ +Ashe,North Carolina,9,37,37009,county,Ashe County,https://www.ashecountygov.com/ +Ashland,Ohio,5,39,39005,county,Ashland County,https://www.ashlandcountyoh.us/ +Ashland,Wisconsin,3,55,55003,county,Ashland County,https://co.ashland.wi.us/ +Ashley,Arkansas,3,5,5003,county,Ashley County,https://www.ashleycountyar.com/ +Ashtabula,Ohio,7,39,39007,county,Ashtabula County,https://www.ashtabulacounty.us/ +Asotin,Washington,3,53,53003,county,Asotin County,https://www.co.asotin.wa.us/ +Assumption,Louisiana,7,22,22007,parish,Assumption Parish,https://www.louisiana.gov/local-louisiana/assumption-parish +Atascosa,Texas,13,48,48013,county,Atascosa County,https://www.atascosacounty.texas.gov/ +Atchison,Missouri,5,29,29005,county,Atchison County,https://atchisoncounty.org/localgovernment/ +Atchison,Kansas,5,20,20005,county,Atchison County,https://www.atchisoncountyks.org/ +Athens,Ohio,9,39,39009,county,Athens County,https://www.co.athensoh.org/ +Atkinson,Georgia,3,13,13003,county,Atkinson County,https://atkinsoncounty.org/ +Atlantic,New Jersey,1,34,34001,county,Atlantic County,https://www.atlantic-county.org/ +Atoka,Oklahoma,5,40,40005,county,Atoka County,https://www.atokaok.org/ +Attala,Mississippi,7,28,28007,county,Attala County,http://www.attalacounty.net/ +Audrain,Missouri,7,29,29007,county,Audrain County,https://www.audraincounty.org/ +Audubon,Iowa,9,19,19009,county,Audubon County,https://www.auduboncountyia.gov/ +Auglaize,Ohio,11,39,39011,county,Auglaize County,https://www2.auglaizecounty.org/ +Augusta,Virginia,15,51,51015,county,Augusta County,https://www.co.augusta.va.us/ +Aurora,South Dakota,3,46,46003,county,Aurora County,https://aurorasd.govoffice3.com/ +Austin,Texas,15,48,48015,county,Austin County,https://www.austincounty.com/ +Autauga,Alabama,1,1,1001,county,Autauga County,https://www.autaugaco.org/ +Avery,North Carolina,11,37,37011,county,Avery County,https://www.averycountync.gov/ +Avoyelles,Louisiana,9,22,22009,parish,Avoyelles Parish,https://www.louisiana.gov/local-louisiana/avoyelles-parish +Baca,Colorado,9,8,8009,county,Baca County,https://www.bacacountyco.gov/ +Bacon,Georgia,5,13,13005,county,Bacon County,https://dfcs.georgia.gov/contacts/bacon-county +Bailey,Texas,17,48,48017,county,Bailey County,https://www.co.bailey.tx.us/ +Baker,Florida,3,12,12003,county,Baker County,https://www.bakercountyfl.org/ +Baker,Oregon,1,41,41001,county,Baker County,https://www.bakercounty.org/ +Baker,Georgia,7,13,13007,county,Baker County,https://www.bakercountyga.com/ +Baldwin,Georgia,9,13,13009,county,Baldwin County,https://www.baldwincountyga.com/home +Baldwin,Alabama,3,1,1003,county,Baldwin County,https://baldwincountyal.gov/ +Ballard,Kentucky,7,21,21007,county,Ballard County,https://ballardcounty.ky.gov/Pages/default.aspx +Baltimore,Maryland,5,24,24005,county,Baltimore County,https://www.baltimorecountymd.gov/ +Baltimore City,Maryland,510,24,24510,city,Baltimore city,https://www.baltimorecity.gov/ +Bamberg,South Carolina,9,45,45009,county,Bamberg County,https://www.bambergcountysc.gov/home +Bandera,Texas,19,48,48019,county,Bandera County,https://www.banderacounty.org/ +Banks,Georgia,11,13,13011,county,Banks County,https://www.bankscountyga.org/home +Banner,Nebraska,7,31,31007,county,Banner County,https://bannercountyne.gov/ +Bannock,Idaho,5,16,16005,county,Bannock County,https://www.bannockcounty.us/ +Baraga,Michigan,13,26,26013,county,Baraga County,https://keweenawbay.org/ +Barber,Kansas,7,20,20007,county,Barber County,https://barber.ks.gov/ +Barbour,West Virginia,1,54,54001,county,Barbour County,https://barbourcountywv.org/ +Barbour,Alabama,5,1,1005,county,Barbour County,https://www.sos.alabama.gov/city-county-lookup/barbour +Barnes,North Dakota,3,38,38003,county,Barnes County,http://www.co.barnes.nd.us/ +Barnstable,Massachusetts,1,25,25001,county,Barnstable County,https://www.capecod.gov/ +Barnwell,South Carolina,11,45,45011,county,Barnwell County,https://www.barnwellcountysc.us/ +Barren,Kentucky,9,21,21009,county,Barren County,https://barrencounty.ky.gov/ +Barron,Wisconsin,5,55,55005,county,Barron County,https://www.barroncountywi.gov/ +Barrow,Georgia,13,13,13013,county,Barrow County,https://www.barrowga.org/ +Barry,Michigan,15,26,26015,county,Barry County,https://www.barrycounty.org/ +Barry,Missouri,9,29,29009,county,Barry County,http://barrycountyassessor.com/ +Bartholomew,Indiana,5,18,18005,county,Bartholomew County,https://www.bartholomew.in.gov/ +Barton,Missouri,11,29,29011,county,Barton County,https://www.bartoncounty.com/ +Barton,Kansas,9,20,20009,county,Barton County,https://www.bartoncounty.org/ +Bartow,Georgia,15,13,13015,county,Bartow County,https://www.bartowcountyga.gov/ +Bastrop,Texas,21,48,48021,county,Bastrop County,https://www.co.bastrop.tx.us/ +Bates,Missouri,13,29,29013,county,Bates County,https://batescounty.net/ +Bath,Kentucky,11,21,21011,county,Bath County,https://bathcounty.ky.gov/ +Bath,Virginia,17,51,51017,county,Bath County,https://www.bathcountyva.gov/ +Baxter,Arkansas,5,5,5005,county,Baxter County,https://www.baxtercountyar.gov/ +Bay,Michigan,17,26,26017,county,Bay County,https://www.baycounty-mi.gov/ +Bay,Florida,5,12,12005,county,Bay County,https://www.co.bay.fl.us/ +Bayfield,Wisconsin,7,55,55007,county,Bayfield County,https://www.bayfieldcounty.wi.gov/ +Baylor,Texas,23,48,48023,county,Baylor County,https://www.co.baylor.tx.us/ +Beadle,South Dakota,5,46,46005,county,Beadle County,https://www.beadlesd.org/ +Bear Lake,Idaho,7,16,16007,county,Bear Lake County,https://www.bearlakecounty.info/ +Beaufort,North Carolina,13,37,37013,county,Beaufort County,https://www.co.beaufort.nc.us/ +Beaufort,South Carolina,13,45,45013,county,Beaufort County,https://www.beaufortcountysc.gov/index.html +Beauregard,Louisiana,11,22,22011,parish,Beauregard Parish,https://www.louisiana.gov/local-louisiana/beauregard-parish +Beaver,Oklahoma,7,40,40007,county,Beaver County,https://beaver.okcounties.org/ +Beaver,Pennsylvania,7,42,42007,county,Beaver County,https://www.beavercountypa.gov/ +Beaver,Utah,1,49,49001,county,Beaver County,https://beaver.utah.gov/ +Beaverhead,Montana,1,30,30001,county,Beaverhead County,https://beaverheadcountymt.gov/ +Becker,Minnesota,5,27,27005,county,Becker County,https://www.co.becker.mn.us/ +Beckham,Oklahoma,9,40,40009,county,Beckham County,https://beckham.okcounties.org/ +Bedford,Pennsylvania,9,42,42009,county,Bedford County,https://www.bedfordcountypa.org/ +Bedford,Tennessee,3,47,47003,county,Bedford County,https://www.bedfordcountytn.gov/ +Bedford,Virginia,19,51,51019,county,Bedford County,https://www.bedfordcountyva.gov/ +Bee,Texas,25,48,48025,county,Bee County,https://www.co.bee.tx.us/ +Belknap,New Hampshire,1,33,33001,county,Belknap County,https://www.belknapcounty.gov/ +Bell,Kentucky,13,21,21013,county,Bell County,https://bellcounty.ky.gov/ +Bell,Texas,27,48,48027,county,Bell County,https://www.bellcountytx.com/ +Belmont,Ohio,13,39,39013,county,Belmont County,https://www.visitbelmontcounty.com/ +Beltrami,Minnesota,7,27,27007,county,Beltrami County,https://www.co.beltrami.mn.us/ +Ben Hill,Georgia,17,13,13017,county,Ben Hill County,https://www.benhillcounty-ga.gov/ +Benewah,Idaho,9,16,16009,county,Benewah County,https://idaho.gov/counties/benewah/ +Bennett,South Dakota,7,46,46007,county,Bennett County,https://www.loc.gov/item/2008622044/ +Bennington,Vermont,3,50,50003,county,Bennington County,https://benningtonvt.org/ +Benson,North Dakota,5,38,38005,county,Benson County,https://www.bensoncountynd.com/ +Bent,Colorado,11,8,8011,county,Bent County,https://www.bentcounty.net/ +Benton,Indiana,7,18,18007,county,Benton County,https://www.bentoncounty.in.gov/ +Benton,Arkansas,7,5,5007,county,Benton County,https://bentoncountyar.gov/ +Benton,Iowa,11,19,19011,county,Benton County,https://www.bentoncountyia.gov/ +Benton,Tennessee,5,47,47005,county,Benton County,https://www.bentoncountytn.gov/ +Benton,Minnesota,9,27,27009,county,Benton County,https://www.co.benton.mn.us/ +Benton,Missouri,15,29,29015,county,Benton County,http://www.bentoncomo.com/ +Benton,Mississippi,9,28,28009,county,Benton County,http://bentoncountyms.gov/ +Benton,Oregon,3,41,41003,county,Benton County,https://www.co.benton.or.us/home +Benton,Washington,5,53,53005,county,Benton County,https://www.co.benton.wa.us/ +Benzie,Michigan,19,26,26019,county,Benzie County,https://www.benzieco.gov/ +Bergen,New Jersey,3,34,34003,county,Bergen County,https://www.co.bergen.nj.us/ +Berkeley,South Carolina,15,45,45015,county,Berkeley County,https://berkeleycountysc.gov/ +Berkeley,West Virginia,3,54,54003,county,Berkeley County,https://www.berkeleywv.org/ +Berks,Pennsylvania,11,42,42011,county,Berks County,https://www.countyofberks.com/ +Berkshire,Massachusetts,3,25,25003,county,Berkshire County,https://www.mass.gov/locations/berkshires-site-office +Bernalillo,New Mexico,1,35,35001,county,Bernalillo County,https://www.bernco.gov/ +Berrien,Michigan,21,26,26021,county,Berrien County,https://www.berriencounty.org/ +Berrien,Georgia,19,13,13019,county,Berrien County,https://berriencountygeorgia.com/ +Bertie,North Carolina,15,37,37015,county,Bertie County,http://www.co.bertie.nc.us/ +Bexar,Texas,29,48,48029,county,Bexar County,https://www.bexar.org/ +Bibb,Alabama,7,1,1007,county,Bibb County,https://bibbal.com/ +Bibb,Georgia,21,13,13021,county,Bibb County,https://www.maconbibb.us/ +Bienville,Louisiana,13,22,22013,parish,Bienville Parish,http://www.bienvilleparish.org/ +Big Horn,Wyoming,3,56,56003,county,Big Horn County,https://www.bighorncountywy.gov/ +Big Horn,Montana,3,30,30003,county,Big Horn County,https://www.bighorncountymt.gov/ +Big Stone,Minnesota,11,27,27011,county,Big Stone County,https://bigstonecounty.gov/ +Billings,North Dakota,7,38,38007,county,Billings County,https://www.billingscountynd.gov/ +Bingham,Idaho,11,16,16011,county,Bingham County,https://www.binghamid.gov/ +Black Hawk,Iowa,13,19,19013,county,Black Hawk County,https://www.blackhawkcounty.iowa.gov/ +Blackford,Indiana,9,18,18009,county,Blackford County,https://www.blackfordcounty.com/ +Bladen,North Carolina,17,37,37017,county,Bladen County,https://bladennc.govoffice3.com/ +Blaine,Nebraska,9,31,31009,county,Blaine County,https://blainecounty.nebraska.gov/welcome +Blaine,Montana,5,30,30005,county,Blaine County,https://blainecounty-mt.gov/ +Blaine,Idaho,13,16,16013,county,Blaine County,https://www.co.blaine.id.us/ +Blaine,Oklahoma,11,40,40011,county,Blaine County,https://blaine.okcounties.org/ +Blair,Pennsylvania,13,42,42013,county,Blair County,https://www.blairco.org/ +Blanco,Texas,31,48,48031,county,Blanco County,https://www.co.blanco.tx.us/ +Bland,Virginia,21,51,51021,county,Bland County,https://www.blandcountyva.gov/ +Bleckley,Georgia,23,13,13023,county,Bleckley County,https://bleckley.org/ +Bledsoe,Tennessee,7,47,47007,county,Bledsoe County,https://bledsoetn.com/ +Blount,Alabama,9,1,1009,county,Blount County,https://blountcountyal.gov/ +Blount,Tennessee,9,47,47009,county,Blount County,https://www.blounttn.gov/ +Blue Earth,Minnesota,13,27,27013,county,Blue Earth County,https://www.blueearthcountymn.gov/ +Boise,Idaho,15,16,16015,county,Boise County,https://www.boisecounty.us/ +Bolivar,Mississippi,11,28,28011,county,Bolivar County,https://www.co.bolivar.ms.us/county-departments +Bollinger,Missouri,17,29,29017,county,Bollinger County,https://www.mocounties.com/bollinger-county +Bon Homme,South Dakota,9,46,46009,county,Bon Homme County,https://bonhomme.sdcounties.org/ +Bond,Illinois,5,17,17005,county,Bond County,https://bondcountyil.gov/ +Bonner,Idaho,17,16,16017,county,Bonner County,https://www.bonnercountyid.gov/ +Bonneville,Idaho,19,16,16019,county,Bonneville County,https://www.bonnevillecountyidaho.gov/ +Boone,Illinois,7,17,17007,county,Boone County,https://www.boonecountyil.gov/ +Boone,West Virginia,5,54,54005,county,Boone County,https://boonecountywv.org/ +Boone,Nebraska,11,31,31011,county,Boone County,https://nebraskacounties.org/nebraska-counties/county/boone.html +Boone,Missouri,19,29,29019,county,Boone County,https://www.showmeboone.com/ +Boone,Arkansas,9,5,5009,county,Boone County,https://www.boonecountyar.com/ +Boone,Kentucky,15,21,21015,county,Boone County,https://www.boonecountyky.org/ +Boone,Indiana,11,18,18011,county,Boone County,https://boonecounty.in.gov/ +Boone,Iowa,15,19,19015,county,Boone County,https://www.boonecounty.iowa.gov/ +Borden,Texas,33,48,48033,county,Borden County,https://www.co.borden.tx.us/ +Bosque,Texas,35,48,48035,county,Bosque County,https://www.bosquecounty.us/ +Bossier,Louisiana,15,22,22015,parish,Bossier Parish,https://www.louisiana.gov/local-louisiana/bossier-parish +Botetourt,Virginia,23,51,51023,county,Botetourt County,https://www.botetourtva.gov/ +Bottineau,North Dakota,9,38,38009,county,Bottineau County,https://www.bottineauco.com/ +Boulder,Colorado,13,8,8013,county,Boulder County,https://bouldercounty.gov/ +Boundary,Idaho,21,16,16021,county,Boundary County,http://boundarycountyid.org/ +Bourbon,Kansas,11,20,20011,county,Bourbon County,https://www.bourboncountyks.org/ +Bourbon,Kentucky,17,21,21017,county,Bourbon County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Bourbon+County +Bowie,Texas,37,48,48037,county,Bowie County,https://www.co.bowie.tx.us/ +Bowman,North Dakota,11,38,38011,county,Bowman County,https://bowmannd.com/ +Box Butte,Nebraska,13,31,31013,county,Box Butte County,https://boxbuttecountyne.gov/ +Box Elder,Utah,3,49,49003,county,Box Elder County,https://www.boxeldercounty.org/home +Boyd,Kentucky,19,21,21019,county,Boyd County,https://boydcountyky.gov/ +Boyd,Nebraska,15,31,31015,county,Boyd County,https://boydcounty.ne.gov/ +Boyle,Kentucky,21,21,21021,county,Boyle County,https://www.boylecountyky.gov/ +Bracken,Kentucky,23,21,21023,county,Bracken County,https://brackencounty.ky.gov/Pages/index.aspx +Bradford,Florida,7,12,12007,county,Bradford County,https://www.bradfordcountyfl.gov/ +Bradford,Pennsylvania,15,42,42015,county,Bradford County,https://bradfordcountypa.org/ +Bradley,Arkansas,11,5,5011,county,Bradley County,https://portal.arkansas.gov/counties/bradley/ +Bradley,Tennessee,11,47,47011,county,Bradley County,https://bradleycountytn.gov/ +Branch,Michigan,23,26,26023,county,Branch County,https://www.countyofbranch.com/ +Brantley,Georgia,25,13,13025,county,Brantley County,https://brantleycounty-ga.gov/ +Braxton,West Virginia,7,54,54007,county,Braxton County,https://braxtonwv.org/ +Brazoria,Texas,39,48,48039,county,Brazoria County,https://www.brazoriacountytx.gov/ +Brazos,Texas,41,48,48041,county,Brazos County,https://www.brazoscountytx.gov/ +Breathitt,Kentucky,25,21,21025,county,Breathitt County,https://breathittcounty.ky.gov/Pages/index.aspx +Breckinridge,Kentucky,27,21,21027,county,Breckinridge County,https://breckinridgeky.com/government/ +Bremer,Iowa,17,19,19017,county,Bremer County,https://www.bremercounty.iowa.gov/ +Brevard,Florida,9,12,12009,county,Brevard County,https://www.brevardfl.gov/ +Brewster,Texas,43,48,48043,county,Brewster County,http://www.brewstercountytx.com/ +Briscoe,Texas,45,48,48045,county,Briscoe County,https://www.co.briscoe.tx.us/ +Bristol,Massachusetts,5,25,25005,county,Bristol County,https://www.countyofbristol.net/ +Bristol,Virginia,520,51,51520,city,Bristol city,https://www.bristolva.org/ +Bristol,Rhode Island,1,44,44001,county,Bristol County,https://www.bristolri.gov/ +Broadwater,Montana,7,30,30007,county,Broadwater County,https://www.broadwatercountymt.com/ +Bronx,New York,5,36,36005,county,Bronx County,https://www.ny.gov/counties/bronx +Brooke,West Virginia,9,54,54009,county,Brooke County,https://br-2.tripod.com/brooke/index.html +Brookings,South Dakota,11,46,46011,county,Brookings County,https://www.brookingscountysd.gov/ +Brooks,Texas,47,48,48047,county,Brooks County,https://www.co.brooks.tx.us/ +Brooks,Georgia,27,13,13027,county,Brooks County,https://brookscountyga.gov/ +Broome,New York,7,36,36007,county,Broome County,https://www.gobroomecounty.com/ +Broomfield,Colorado,14,8,8014,county,Broomfield County,https://broomfield.org/ +Broward,Florida,11,12,12011,county,Broward County,https://www.broward.org/ +Brown,Indiana,13,18,18013,county,Brown County,https://www.browncounty.com/ +Brown,Minnesota,15,27,27015,county,Brown County,https://www.co.brown.mn.us/ +Brown,Kansas,13,20,20013,county,Brown County,https://www.brcoks.org/ +Brown,South Dakota,13,46,46013,county,Brown County,https://www.brown.sd.us/ +Brown,Wisconsin,9,55,55009,county,Brown County,https://www.browncountywi.gov/ +Brown,Texas,49,48,48049,county,Brown County,https://www.browncountytx.gov/ +Brown,Illinois,9,17,17009,county,Brown County,https://www.browncoil.org/ +Brown,Nebraska,17,31,31017,county,Brown County,https://browncounty.ne.gov/ +Brown,Ohio,15,39,39015,county,Brown County,http://www.browncountyohio.gov/ +Brule,South Dakota,15,46,46015,county,Brule County,https://www.districtiii.org/district/brule.php +Brunswick,North Carolina,19,37,37019,county,Brunswick County,https://www.brunswickcountync.gov/ +Brunswick,Virginia,25,51,51025,county,Brunswick County,https://brunswickco.com/ +Bryan,Georgia,29,13,13029,county,Bryan County,https://www.bryancountyga.org/ +Bryan,Oklahoma,13,40,40013,county,Bryan County,https://oklahoma.gov/health/locations/county-health-departments/bryan-county-health-department.html +Buchanan,Missouri,21,29,29021,county,Buchanan County,https://www.co.buchanan.mo.us/ +Buchanan,Virginia,27,51,51027,county,Buchanan County,https://www.buchanancountyonline.com/ +Buchanan,Iowa,19,19,19019,county,Buchanan County,https://www.buchanancounty.iowa.gov/ +Buckingham,Virginia,29,51,51029,county,Buckingham County,https://www.buckinghamcountyva.org/ +Bucks,Pennsylvania,17,42,42017,county,Bucks County,https://www.buckscounty.gov/ +Buena Vista,Virginia,530,51,51530,city,Buena Vista city,https://www.buenavistava.org/ +Buena Vista,Iowa,21,19,19021,county,Buena Vista County,https://buenavistacounty.iowa.gov/ +Buffalo,Wisconsin,11,55,55011,county,Buffalo County,https://www.buffalocountywi.gov/ +Buffalo,South Dakota,17,46,46017,county,Buffalo County,https://buffalo.sdcounties.org/ +Buffalo,Nebraska,19,31,31019,county,Buffalo County,https://buffalocounty.ne.gov/ +Bullitt,Kentucky,29,21,21029,county,Bullitt County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Bullitt+County +Bulloch,Georgia,31,13,13031,county,Bulloch County,https://bullochcounty.net/ +Bullock,Alabama,11,1,1011,county,Bullock County,https://www.bullockal.com/ +Buncombe,North Carolina,21,37,37021,county,Buncombe County,https://www.buncombecounty.org/ +Bureau,Illinois,11,17,17011,county,Bureau County,https://www.bureaucounty-il.gov/ +Burke,North Carolina,23,37,37023,county,Burke County,https://www.burkenc.org/ +Burke,North Dakota,13,38,38013,county,Burke County,https://burkecountynd.com/ +Burke,Georgia,33,13,13033,county,Burke County,https://burkecounty-ga.gov/ +Burleigh,North Dakota,15,38,38015,county,Burleigh County,https://www.burleighco.com/ +Burleson,Texas,51,48,48051,county,Burleson County,https://www.co.burleson.tx.us/ +Burlington,New Jersey,5,34,34005,county,Burlington County,https://www.co.burlington.nj.us/ +Burnet,Texas,53,48,48053,county,Burnet County,https://www.burnetcountytexas.org/ +Burnett,Wisconsin,13,55,55013,county,Burnett County,https://www.burnettcountywi.gov/ +Burt,Nebraska,21,31,31021,county,Burt County,https://burtcounty.ne.gov/ +Butler,Alabama,13,1,1013,county,Butler County,http://www.butleralabama.org/ +Butler,Iowa,23,19,19023,county,Butler County,https://butlercounty.iowa.gov/ +Butler,Pennsylvania,19,42,42019,county,Butler County,https://www.butlercountypa.gov/ +Butler,Ohio,17,39,39017,county,Butler County,https://www.bcohio.gov/ +Butler,Nebraska,23,31,31023,county,Butler County,https://butlercountyne.gov/ +Butler,Kansas,15,20,20015,county,Butler County,https://www.bucoks.com/ +Butler,Kentucky,31,21,21031,county,Butler County,https://butlercounty.ky.gov/Pages/default.aspx +Butler,Missouri,23,29,29023,county,Butler County,https://butlercountymo.com/ +Butte,California,7,6,6007,county,Butte County,https://www.buttecounty.net/ +Butte,Idaho,23,16,16023,county,Butte County,https://idaho.gov/counties/butte/ +Butte,South Dakota,19,46,46019,county,Butte County,https://www.buttesd.org/ +Butts,Georgia,35,13,13035,county,Butts County,https://buttscountyga.com/ +Cabarrus,North Carolina,25,37,37025,county,Cabarrus County,https://www.cabarruscounty.us/Home +Cabell,West Virginia,11,54,54011,county,Cabell County,http://www.cabellcounty.org/ +Cache,Utah,5,49,49005,county,Cache County,https://www.cachecounty.org/ +Caddo,Oklahoma,15,40,40015,county,Caddo County,https://oklahoma.gov/okdhs/library/resources/caddord211.html +Caddo,Louisiana,17,22,22017,parish,Caddo Parish,http://www.caddo.org/ +Calaveras,California,9,6,6009,county,Calaveras County,https://www.calaverasgov.us/ +Calcasieu,Louisiana,19,22,22019,parish,Calcasieu Parish,https://www.louisiana.gov/local-louisiana/calcasieu-parish +Caldwell,Texas,55,48,48055,county,Caldwell County,https://www.co.caldwell.tx.us/ +Caldwell,Missouri,25,29,29025,county,Caldwell County,https://www.caldwellco.missouri.org/ +Caldwell,North Carolina,27,37,37027,county,Caldwell County,https://www.caldwellcountync.org/ +Caldwell,Louisiana,21,22,22021,parish,Caldwell Parish,https://www.louisiana.gov/local-louisiana/caldwell-parish +Caldwell,Kentucky,33,21,21033,county,Caldwell County,https://caldwellcounty.ky.gov/Pages/index.aspx +Caledonia,Vermont,5,50,50005,county,Caledonia County,http://bgs.vermont.gov/facilities/east/caledoniacourt +Calhoun,Michigan,25,26,26025,county,Calhoun County,https://www.calhouncountymi.gov/ +Calhoun,Arkansas,13,5,5013,county,Calhoun County,https://local.arkansas.gov/local.php?agency=Calhoun%20County +Calhoun,Georgia,37,13,13037,county,Calhoun County,https://calhouncountyga.com/ +Calhoun,Texas,57,48,48057,county,Calhoun County,https://www.calhouncotx.org/ +Calhoun,Illinois,13,17,17013,county,Calhoun County,https://www.ilsos.gov/departments/archives/IRAD/calhoun.html +Calhoun,Mississippi,13,28,28013,county,Calhoun County,http://www.calhounso.org/page.php?id=4 +Calhoun,West Virginia,13,54,54013,county,Calhoun County,https://calhouncounty.wv.gov/ +Calhoun,Iowa,25,19,19025,county,Calhoun County,https://www.calhouncounty.iowa.gov/ +Calhoun,Florida,13,12,12013,county,Calhoun County,https://www.calhouncountyfl.gov/ +Calhoun,Alabama,15,1,1015,county,Calhoun County,https://www.calhouncounty.org/ +Calhoun,South Carolina,17,45,45017,county,Calhoun County,https://calhouncounty.sc.gov/ +Callahan,Texas,59,48,48059,county,Callahan County,https://www.callahancounty.org/ +Callaway,Missouri,27,29,29027,county,Callaway County,https://callawaycounty.org/ +Calloway,Kentucky,35,21,21035,county,Calloway County,https://callowaycountyky.gov/ +Calumet,Wisconsin,15,55,55015,county,Calumet County,https://www.co.calumet.wi.us/ +Calvert,Maryland,9,24,24009,county,Calvert County,https://www.calvertcountymd.gov/ +Camas,Idaho,25,16,16025,county,Camas County,http://camascounty.id.gov/ +Cambria,Pennsylvania,21,42,42021,county,Cambria County,https://www.cambriacountypa.gov/ +Camden,Missouri,29,29,29029,county,Camden County,https://www.camdenmo.org/ +Camden,New Jersey,7,34,34007,county,Camden County,https://www.camdencounty.com/ +Camden,North Carolina,29,37,37029,county,Camden County,https://www.camdencountync.gov/ +Camden,Georgia,39,13,13039,county,Camden County,https://www.camdencountyga.gov/ +Cameron,Louisiana,23,22,22023,parish,Cameron Parish,https://www.louisiana.gov/local-louisiana/cameron-parish +Cameron,Texas,61,48,48061,county,Cameron County,https://www.cameroncountytx.gov/ +Cameron,Pennsylvania,23,42,42023,county,Cameron County,https://www.cameroncountypa.com/ +Camp,Texas,63,48,48063,county,Camp County,https://www.co.camp.tx.us/ +Campbell,Kentucky,37,21,21037,county,Campbell County,https://campbellcountyky.gov/ +Campbell,South Dakota,21,46,46021,county,Campbell County,https://ujs.sd.gov/Fifth_Circuit/Links/Counties.aspx?1Oz6c6VS4cZ5q%2FI%2BN1IFTHT8EWt%2FjRd0FRZDldYePio%3D +Campbell,Virginia,31,51,51031,county,Campbell County,https://www.co.campbell.va.us/ +Campbell,Tennessee,13,47,47013,county,Campbell County,https://campbellcountytn.gov/ +Campbell,Wyoming,5,56,56005,county,Campbell County,https://www.campbellcountywy.gov/ +Canadian,Oklahoma,17,40,40017,county,Canadian County,https://www.canadiancounty.org/ +Candler,Georgia,43,13,13043,county,Candler County,https://metter-candlercounty.com/ +Cannon,Tennessee,15,47,47015,county,Cannon County,https://cannoncountytn.gov/ +Canyon,Idaho,27,16,16027,county,Canyon County,https://www.canyoncounty.id.gov/ +Cape Girardeau,Missouri,31,29,29031,county,Cape Girardeau County,https://www.capecounty.us/ +Cape May,New Jersey,9,34,34009,county,Cape May County,https://capemaycountynj.gov/ +Capitol,Connecticut,110,9,9110,planning region,Capitol Planning Region,"https://en.wikipedia.org/wiki/Capitol_Planning_Region,_Connecticut" +Carbon,Pennsylvania,25,42,42025,county,Carbon County,https://www.carboncountypa.gov/ +Carbon,Wyoming,7,56,56007,county,Carbon County,https://www.carbonwy.com/ +Carbon,Utah,7,49,49007,county,Carbon County,https://www.carbonutah.com/ +Carbon,Montana,9,30,30009,county,Carbon County,https://co.carbon.mt.us/ +Caribou,Idaho,29,16,16029,county,Caribou County,https://www.cariboucounty.us/ +Carlisle,Kentucky,39,21,21039,county,Carlisle County,https://carlislecounty.ky.gov/Pages/default.aspx +Carlton,Minnesota,17,27,27017,county,Carlton County,https://www.co.carlton.mn.us/ +Caroline,Maryland,11,24,24011,county,Caroline County,https://www.carolinemd.org/ +Caroline,Virginia,33,51,51033,county,Caroline County,https://co.caroline.va.us/ +Carroll,Arkansas,15,5,5015,county,Carroll County,http://carrollcounty.us/ +Carroll,Georgia,45,13,13045,county,Carroll County,https://carrollcountyga.com/ +Carroll,Maryland,13,24,24013,county,Carroll County,https://www.carrollcountymd.gov/ +Carroll,Tennessee,17,47,47017,county,Carroll County,https://carrollcountytn.gov/ +Carroll,Mississippi,15,28,28015,county,Carroll County,https://carrollcountyms.org/county-officials/ +Carroll,Kentucky,41,21,21041,county,Carroll County,http://www.carrollcountygov.us/ +Carroll,Iowa,27,19,19027,county,Carroll County,https://www.carrollcountyiowa.gov/ +Carroll,Illinois,15,17,17015,county,Carroll County,https://www.carrollcountyil.gov/ +Carroll,Indiana,15,18,18015,county,Carroll County,https://www.carrollcountyindiana.com/ +Carroll,Virginia,35,51,51035,county,Carroll County,https://carrollcountyva.gov/ +Carroll,New Hampshire,3,33,33003,county,Carroll County,https://www.carrollcountynh.net/ +Carroll,Ohio,19,39,39019,county,Carroll County,https://carrollcountyohio.us/ +Carroll,Missouri,33,29,29033,county,Carroll County,https://www.carrollcountymo.gov/ +Carson,Texas,65,48,48065,county,Carson County,https://www.co.carson.tx.us/ +Carson City,Nevada,510,32,32510,city,Carson City,https://www.carson.org/ +Carter,Oklahoma,19,40,40019,county,Carter County,https://cartercountyok.us/ +Carter,Tennessee,19,47,47019,county,Carter County,https://www.cartercountytn.gov/ +Carter,Missouri,35,29,29035,county,Carter County,https://www.cartercountymosheriff.org/page.php?id=6 +Carter,Kentucky,43,21,21043,county,Carter County,https://cartercounty.ky.gov/ +Carter,Montana,11,30,30011,county,Carter County,https://cartercountymontana.squarespace.com/ +Carteret,North Carolina,31,37,37031,county,Carteret County,https://www.carteretcountync.gov/ +Carver,Minnesota,19,27,27019,county,Carver County,https://www.carvercountymn.gov/ +Cascade,Montana,13,30,30013,county,Cascade County,https://www.cascadecountymt.gov/ +Casey,Kentucky,45,21,21045,county,Casey County,https://casey.countyclerk.us/ +Cass,Minnesota,21,27,27021,county,Cass County,https://www.casscountymn.gov/ +Cass,Michigan,27,26,26027,county,Cass County,https://www.casscountymi.org/ +Cass,Missouri,37,29,29037,county,Cass County,https://www.casscounty.com/ +Cass,Illinois,17,17,17017,county,Cass County,https://co.cass.il.us/ +Cass,North Dakota,17,38,38017,county,Cass County,https://www.casscountynd.gov/ +Cass,Iowa,29,19,19029,county,Cass County,https://www.casscountyia.gov/ +Cass,Nebraska,25,31,31025,county,Cass County,https://www.cassne.org/ +Cass,Texas,67,48,48067,county,Cass County,https://www.co.cass.tx.us/ +Cass,Indiana,17,18,18017,county,Cass County,https://www.co.cass.in.us/ +Cassia,Idaho,31,16,16031,county,Cassia County,https://www.cassia.gov/ +Castro,Texas,69,48,48069,county,Castro County,https://www.co.castro.tx.us/ +Caswell,North Carolina,33,37,37033,county,Caswell County,https://www.caswellcountync.gov/ +Catahoula,Louisiana,25,22,22025,parish,Catahoula Parish,https://www.louisiana.gov/local-louisiana/catahoula-parish +Catawba,North Carolina,35,37,37035,county,Catawba County,https://www.catawbacountync.gov/ +Catoosa,Georgia,47,13,13047,county,Catoosa County,https://www.catoosa.com/ +Catron,New Mexico,3,35,35003,county,Catron County,https://www.catroncounty.us/ +Cattaraugus,New York,9,36,36009,county,Cattaraugus County,https://www.cattco.org/ +Cavalier,North Dakota,19,38,38019,county,Cavalier County,https://cavaliercounty.us/ +Cayuga,New York,11,36,36011,county,Cayuga County,https://www.cayugacounty.us/ +Cecil,Maryland,15,24,24015,county,Cecil County,https://www.ccgov.org/ +Cedar,Nebraska,27,31,31027,county,Cedar County,https://cedarcountyne.gov/ +Cedar,Missouri,39,29,29039,county,Cedar County,https://cedarcountymo.gov/ +Cedar,Iowa,31,19,19031,county,Cedar County,https://cedarcounty.iowa.gov/ +Centre,Pennsylvania,27,42,42027,county,Centre County,https://centrecountypa.gov/ +Cerro Gordo,Iowa,33,19,19033,county,Cerro Gordo County,https://www.cgcounty.org/ +Chaffee,Colorado,15,8,8015,county,Chaffee County,https://www.chaffeecounty.org/ +Chambers,Texas,71,48,48071,county,Chambers County,https://www.co.chambers.tx.us/ +Chambers,Alabama,17,1,1017,county,Chambers County,https://www.chamberscountyal.gov/ +Champaign,Illinois,19,17,17019,county,Champaign County,https://www.co.champaign.il.us/headermenu/home.php +Champaign,Ohio,21,39,39021,county,Champaign County,https://www.co.champaign.oh.us/ +Chariton,Missouri,41,29,29041,county,Chariton County,https://www.charitonco.com/ +Charles,Maryland,17,24,24017,county,Charles County,https://www.charlescountymd.gov/ +Charles City,Virginia,36,51,51036,county,Charles City County,https://www.charlescityva.us/ +Charles Mix,South Dakota,23,46,46023,county,Charles Mix County,https://charlesmix.sdcounties.org/ +Charleston,South Carolina,19,45,45019,county,Charleston County,https://www.charlestoncounty.org/ +Charlevoix,Michigan,29,26,26029,county,Charlevoix County,https://www.charlevoixcounty.org/ +Charlotte,Virginia,37,51,51037,county,Charlotte County,https://www.charlottecountyva.gov/ +Charlotte,Florida,15,12,12015,county,Charlotte County,https://www.charlottecountyfl.gov/ +Charlottesville,Virginia,540,51,51540,city,Charlottesville city,"https://www.niche.com/places-to-live/charlottesville-va/#:~:text=Charlottesville%20is%20a%20town%20in,most%20residents%20rent%20their%20homes." +Charlton,Georgia,49,13,13049,county,Charlton County,https://charltoncountyga.us/ +Chase,Kansas,17,20,20017,county,Chase County,https://chasecountyks.com/ +Chase,Nebraska,29,31,31029,county,Chase County,https://chasecounty.nebraska.gov/ +Chatham,Georgia,51,13,13051,county,Chatham County,https://www.chathamcountyga.gov/ +Chatham,North Carolina,37,37,37037,county,Chatham County,https://www.chathamcountync.gov/ +Chattahoochee,Georgia,53,13,13053,county,Chattahoochee County,https://ugoccc.com/ +Chattooga,Georgia,55,13,13055,county,Chattooga County,https://chattoogacounty.org/ +Chautauqua,Kansas,19,20,20019,county,Chautauqua County,https://www.chautauquacountyks.com/ +Chautauqua,New York,13,36,36013,county,Chautauqua County,https://chqgov.com/ +Chaves,New Mexico,5,35,35005,county,Chaves County,https://www.chavescounty.gov/ +Cheatham,Tennessee,21,47,47021,county,Cheatham County,https://www.cheathamcountytn.gov/ +Cheboygan,Michigan,31,26,26031,county,Cheboygan County,https://www.cheboygancounty.net/ +Chelan,Washington,7,53,53007,county,Chelan County,https://www.co.chelan.wa.us/ +Chemung,New York,15,36,36015,county,Chemung County,https://www.chemungcountyny.gov/ +Chenango,New York,17,36,36017,county,Chenango County,http://www.co.chenango.ny.us/ +Cherokee,Oklahoma,21,40,40021,county,Cherokee County,https://oklahoma.gov/okdhs/library/resources/cherokeerd211.html +Cherokee,Texas,73,48,48073,county,Cherokee County,http://www.co.cherokee.tx.us/ +Cherokee,Georgia,57,13,13057,county,Cherokee County,https://www.cherokeega.com/ +Cherokee,Iowa,35,19,19035,county,Cherokee County,https://www.cherokeecounty.iowa.gov/ +Cherokee,North Carolina,39,37,37039,county,Cherokee County,https://www.cherokeecounty-nc.gov/ +Cherokee,Kansas,21,20,20021,county,Cherokee County,http://cherokeecountyks.gov/ +Cherokee,Alabama,19,1,1019,county,Cherokee County,https://cherokeecounty-al.gov/ +Cherokee,South Carolina,21,45,45021,county,Cherokee County,https://cherokeecountysc.gov/ +Cherry,Nebraska,31,31,31031,county,Cherry County,https://cherrycountyne.gov/ +Chesapeake,Virginia,550,51,51550,city,Chesapeake city,https://www.cityofchesapeake.net/ +Cheshire,New Hampshire,5,33,33005,county,Cheshire County,https://co.cheshire.nh.us/ +Chester,South Carolina,23,45,45023,county,Chester County,https://www.chestercountysc.gov/ +Chester,Pennsylvania,29,42,42029,county,Chester County,https://www.chesco.org/ +Chester,Tennessee,23,47,47023,county,Chester County,https://chestercountytn.org/ +Chesterfield,South Carolina,25,45,45025,county,Chesterfield County,https://www.chesterfieldcountysc.com/ +Chesterfield,Virginia,41,51,51041,county,Chesterfield County,https://www.chesterfield.gov/ +Cheyenne,Colorado,17,8,8017,county,Cheyenne County,https://www.co.cheyenne.co.us/ +Cheyenne,Kansas,23,20,20023,county,Cheyenne County,https://cncoks.us/ +Cheyenne,Nebraska,33,31,31033,county,Cheyenne County,https://www.cheyennecountyne.net/ +Chickasaw,Iowa,37,19,19037,county,Chickasaw County,https://chickasawcounty.iowa.gov/ +Chickasaw,Mississippi,17,28,28017,county,Chickasaw County,https://www.houston.ms.gov/ +Chicot,Arkansas,17,5,5017,county,Chicot County,https://local.arkansas.gov/local.php?agency=Chicot%20County +Childress,Texas,75,48,48075,county,Childress County,http://www.childresscountytexas.us/ +Chilton,Alabama,21,1,1021,county,Chilton County,https://chiltoncounty.org/ +Chippewa,Michigan,33,26,26033,county,Chippewa County,https://www.chippewacountymi.gov/ +Chippewa,Minnesota,23,27,27023,county,Chippewa County,https://www.co.chippewa.mn.us/ +Chippewa,Wisconsin,17,55,55017,county,Chippewa County,https://www.co.chippewa.wi.us/ +Chisago,Minnesota,25,27,27025,county,Chisago County,https://www.chisagocountymn.gov/ +Chittenden,Vermont,7,50,50007,county,Chittenden County,http://www.vermont.gov/government +Choctaw,Alabama,23,1,1023,county,Choctaw County,https://www.sos.alabama.gov/city-county-lookup/choctaw-0 +Choctaw,Mississippi,19,28,28019,county,Choctaw County,https://www.mssupervisors.org/ms-counties/choctaw +Choctaw,Oklahoma,23,40,40023,county,Choctaw County,https://oklahoma.gov/health/locations/county-health-departments/choctaw-county-health-department.html +Chouteau,Montana,15,30,30015,county,Chouteau County,https://co.chouteau.mt.us/ +Chowan,North Carolina,41,37,37041,county,Chowan County,https://www.chowancounty-nc.gov/ +Christian,Missouri,43,29,29043,county,Christian County,https://www.christiancountymo.gov/ +Christian,Illinois,21,17,17021,county,Christian County,https://www.christiancountyil.gov/ +Christian,Kentucky,47,21,21047,county,Christian County,https://christiancountyky.gov/ +Churchill,Nevada,1,32,32001,county,Churchill County,https://www.churchillcountynv.gov/ +Cibola,New Mexico,6,35,35006,county,Cibola County,https://www.cibolacountynm.com/ +Cimarron,Oklahoma,25,40,40025,county,Cimarron County,https://oklahoma.gov/odot/about/contact-us/field-district/district-6/div-6-cimarron-county-.html +Citrus,Florida,17,12,12017,county,Citrus County,https://www.citrusbocc.com/ +Clackamas,Oregon,5,41,41005,county,Clackamas County,https://www.clackamas.us/ +Claiborne,Tennessee,25,47,47025,county,Claiborne County,https://claibornecountytn.gov/ +Claiborne,Mississippi,21,28,28021,county,Claiborne County,https://www.ccmsgov.us/ +Claiborne,Louisiana,27,22,22027,parish,Claiborne Parish,https://www.louisiana.gov/local-louisiana/claiborne-parish +Clallam,Washington,9,53,53009,county,Clallam County,https://www.clallamcountywa.gov/ +Clare,Michigan,35,26,26035,county,Clare County,https://clareco.net/ +Clarendon,South Carolina,27,45,45027,county,Clarendon County,https://www.clarendoncountygov.org/ +Clarion,Pennsylvania,31,42,42031,county,Clarion County,https://www.co.clarion.pa.us/ +Clark,Ohio,23,39,39023,county,Clark County,https://www.clarkcountyohio.gov/ +Clark,Illinois,23,17,17023,county,Clark County,https://www.clarkcountyil.org/ +Clark,Missouri,45,29,29045,county,Clark County,http://clarkcountymo.org/ +Clark,Kentucky,49,21,21049,county,Clark County,https://www.clarkcoky.com/ +Clark,South Dakota,25,46,46025,county,Clark County,https://clark.sdcounties.org/ +Clark,Wisconsin,19,55,55019,county,Clark County,https://www.clarkcountywi.gov/ +Clark,Idaho,33,16,16033,county,Clark County,https://www.clark-co.id.gov/ +Clark,Nevada,3,32,32003,county,Clark County,https://www.clarkcountynv.gov/ +Clark,Indiana,19,18,18019,county,Clark County,https://www.co.clark.in.us/ +Clark,Washington,11,53,53011,county,Clark County,https://clark.wa.gov/ +Clark,Kansas,25,20,20025,county,Clark County,https://www.clarkcountyks.com/ +Clark,Arkansas,19,5,5019,county,Clark County,https://clarkcountyar.gov/ +Clarke,Iowa,39,19,19039,county,Clarke County,https://clarkecounty.iowa.gov/ +Clarke,Georgia,59,13,13059,county,Clarke County,https://accgov.com/ +Clarke,Mississippi,23,28,28023,county,Clarke County,https://clarkecountyms.gov/ +Clarke,Virginia,43,51,51043,county,Clarke County,https://www.clarkecounty.gov/ +Clarke,Alabama,25,1,1025,county,Clarke County,https://clarkecountyal.com/ +Clatsop,Oregon,7,41,41007,county,Clatsop County,http://www.clatsopcounty.gov/ +Clay,Minnesota,27,27,27027,county,Clay County,https://claycountymn.gov/ +Clay,Nebraska,35,31,31035,county,Clay County,https://claycounty.ne.gov/ +Clay,North Carolina,43,37,37043,county,Clay County,https://www.clayconc.com/ +Clay,Missouri,47,29,29047,county,Clay County,https://www.claycountymo.gov/ +Clay,Texas,77,48,48077,county,Clay County,https://www.co.clay.tx.us/ +Clay,Georgia,61,13,13061,county,Clay County,https://www.claycountyga.net/ +Clay,Kentucky,51,21,21051,county,Clay County,https://claycounty.ky.gov/ +Clay,Florida,19,12,12019,county,Clay County,https://www.claycountygov.com/ +Clay,Mississippi,25,28,28025,county,Clay County,http://www.claycountyms.com/ +Clay,West Virginia,15,54,54015,county,Clay County,https://claycounty.wv.gov/ +Clay,Alabama,27,1,1027,county,Clay County,https://alabamaclaycounty.com/ +Clay,Tennessee,27,47,47027,county,Clay County,https://visitclaycountytn.com/project/government/ +Clay,Kansas,27,20,20027,county,Clay County,https://www.claycountykansas.org/ +Clay,Arkansas,21,5,5021,county,Clay County,https://www.claycountyarkansas.org/ +Clay,Iowa,41,19,19041,county,Clay County,https://claycounty.iowa.gov/ +Clay,Indiana,21,18,18021,county,Clay County,https://www.claycountyin.gov/ +Clay,South Dakota,27,46,46027,county,Clay County,https://www.claycountysd.org/ +Clay,Illinois,25,17,17025,county,Clay County,https://claycountyillinois.org/ +Clayton,Georgia,63,13,13063,county,Clayton County,https://www.claytoncountyga.gov/ +Clayton,Iowa,43,19,19043,county,Clayton County,https://www.claytoncountyia.gov/ +Clear Creek,Colorado,19,8,8019,county,Clear Creek County,https://www.clearcreekcounty.us/ +Clearfield,Pennsylvania,33,42,42033,county,Clearfield County,https://clearfieldco.org/ +Clearwater,Minnesota,29,27,27029,county,Clearwater County,https://www.co.clearwater.mn.us/ +Clearwater,Idaho,35,16,16035,county,Clearwater County,https://www.clearwatercounty.org/ +Cleburne,Alabama,29,1,1029,county,Cleburne County,https://www.cleburnecounty.us/ +Cleburne,Arkansas,23,5,5023,county,Cleburne County,https://www.cleburnecountyar.com/ +Clermont,Ohio,25,39,39025,county,Clermont County,https://clermontcountyohio.gov/ +Cleveland,Arkansas,25,5,5025,county,Cleveland County,https://local.arkansas.gov/local.php?agency=Cleveland%20County +Cleveland,Oklahoma,27,40,40027,county,Cleveland County,https://clevelandcountyok.com/ +Cleveland,North Carolina,45,37,37045,county,Cleveland County,https://www.clevelandcounty.com/ +Clinch,Georgia,65,13,13065,county,Clinch County,https://clinchcountyga.gov/index.html +Clinton,Iowa,45,19,19045,county,Clinton County,https://www.clintoncounty-ia.gov/ +Clinton,Pennsylvania,35,42,42035,county,Clinton County,https://www.clintoncountypa.gov/ +Clinton,Kentucky,53,21,21053,county,Clinton County,https://clintoncounty.ky.gov/ +Clinton,Indiana,23,18,18023,county,Clinton County,https://www.in.gov/core/mylocal/clinton_county.html +Clinton,Michigan,37,26,26037,county,Clinton County,https://www.clinton-county.org/ +Clinton,Illinois,27,17,17027,county,Clinton County,https://clintonco.illinois.gov/ +Clinton,Missouri,49,29,29049,county,Clinton County,https://clintoncomo.org/ +Clinton,Ohio,27,39,39027,county,Clinton County,https://co.clinton.oh.us/ +Clinton,New York,19,36,36019,county,Clinton County,https://www.clintoncountygov.com/ +Cloud,Kansas,29,20,20029,county,Cloud County,https://www.cloudcountyks.org/ +Coahoma,Mississippi,27,28,28027,county,Coahoma County,https://coahomacounty.net/ +Coal,Oklahoma,29,40,40029,county,Coal County,https://coal.okcounties.org/ +Cobb,Georgia,67,13,13067,county,Cobb County,https://www.cobbcounty.org/ +Cochise,Arizona,3,4,4003,county,Cochise County,https://www.cochise.az.gov/ +Cochran,Texas,79,48,48079,county,Cochran County,https://www.co.cochran.tx.us/ +Cocke,Tennessee,29,47,47029,county,Cocke County,https://www.cockecountytn.gov/ +Coconino,Arizona,5,4,4005,county,Coconino County,https://www.coconino.az.gov/ +Codington,South Dakota,29,46,46029,county,Codington County,https://www.codington.org/ +Coffee,Tennessee,31,47,47031,county,Coffee County,https://www.coffeecountytn.gov/ +Coffee,Georgia,69,13,13069,county,Coffee County,https://www.coffeecountypay.com/ +Coffee,Alabama,31,1,1031,county,Coffee County,https://www.coffeecounty.us/ +Coffey,Kansas,31,20,20031,county,Coffey County,https://www.coffeycountyks.org/ +Coke,Texas,81,48,48081,county,Coke County,https://www.co.coke.tx.us/ +Colbert,Alabama,33,1,1033,county,Colbert County,https://www.colbertcounty.org/ +Cole,Missouri,51,29,29051,county,Cole County,https://www.colecounty.org/ +Coleman,Texas,83,48,48083,county,Coleman County,https://www.co.coleman.tx.us/ +Coles,Illinois,29,17,17029,county,Coles County,https://www.colesco.illinois.gov/ +Colfax,Nebraska,37,31,31037,county,Colfax County,https://colfaxcountyne.gov/ +Colfax,New Mexico,7,35,35007,county,Colfax County,https://www.co.colfax.nm.us/ +Colleton,South Carolina,29,45,45029,county,Colleton County,https://www.colletoncounty.org/ +Collier,Florida,21,12,12021,county,Collier County,https://www.colliercountyfl.gov/ +Collin,Texas,85,48,48085,county,Collin County,https://www.collincountytx.gov/ +Collingsworth,Texas,87,48,48087,county,Collingsworth County,https://www.co.collingsworth.tx.us/ +Colonial Heights,Virginia,570,51,51570,city,Colonial Heights city,https://www.colonialheightsva.gov/ +Colorado,Texas,89,48,48089,county,Colorado County,https://www.co.colorado.tx.us/ +Colquitt,Georgia,71,13,13071,county,Colquitt County,https://www.colquittcountyga.gov/ +Columbia,Florida,23,12,12023,county,Columbia County,https://www.columbiacountyfla.com/ +Columbia,New York,21,36,36021,county,Columbia County,https://www.columbiacountyny.com/ +Columbia,Washington,13,53,53013,county,Columbia County,https://www.columbiaco.com/ +Columbia,Oregon,9,41,41009,county,Columbia County,https://www.columbiacountyor.gov/ +Columbia,Wisconsin,21,55,55021,county,Columbia County,https://www.co.columbia.wi.us/ +Columbia,Arkansas,27,5,5027,county,Columbia County,https://www.countyofcolumbia.org/ +Columbia,Georgia,73,13,13073,county,Columbia County,https://www.columbiacountyga.gov/ +Columbia,Pennsylvania,37,42,42037,county,Columbia County,http://www.columbiapa.org/ +Columbiana,Ohio,29,39,39029,county,Columbiana County,http://www.columbianacounty.org/ +Columbus,North Carolina,47,37,37047,county,Columbus County,https://columbusco.org/ +Colusa,California,11,6,6011,county,Colusa County,https://www.countyofcolusa.org/ +Comal,Texas,91,48,48091,county,Comal County,https://www.co.comal.tx.us/ +Comanche,Kansas,33,20,20033,county,Comanche County,http://www.comanchecoks.org/ +Comanche,Texas,93,48,48093,county,Comanche County,https://www.co.comanche.tx.us/ +Comanche,Oklahoma,31,40,40031,county,Comanche County,https://www.comanchecounty.us/ +Concho,Texas,95,48,48095,county,Concho County,https://www.co.concho.tx.us/ +Concordia,Louisiana,29,22,22029,parish,Concordia Parish,https://www.louisiana.gov/local-louisiana/concordia-parish +Conecuh,Alabama,35,1,1035,county,Conecuh County,https://www.conecuhcounty.us/ +Conejos,Colorado,21,8,8021,county,Conejos County,https://conejoscounty.colorado.gov/ +Contra Costa,California,13,6,6013,county,Contra Costa County,https://www.contracosta.ca.gov/ +Converse,Wyoming,9,56,56009,county,Converse County,https://www.conversecountywy.gov/ +Conway,Arkansas,29,5,5029,county,Conway County,https://conwaycountyar.com/ +Cook,Illinois,31,17,17031,county,Cook County,https://www.cookcountyil.gov/ +Cook,Georgia,75,13,13075,county,Cook County,https://cookcountyga.us/ +Cook,Minnesota,31,27,27031,county,Cook County,https://www.co.cook.mn.us/ +Cooke,Texas,97,48,48097,county,Cooke County,https://www.co.cooke.tx.us/ +Cooper,Missouri,53,29,29053,county,Cooper County,https://www.coopercountymo.gov/ +Coos,Oregon,11,41,41011,county,Coos County,https://www.co.coos.or.us/home +Coos,New Hampshire,7,33,33007,county,Coos County,https://www.cooscountynh.us/ +Coosa,Alabama,37,1,1037,county,Coosa County,http://www.coosacountyal.com/ +Copiah,Mississippi,29,28,28029,county,Copiah County,https://copiahcounty.org/ +Corson,South Dakota,31,46,46031,county,Corson County,https://corson.sdcounties.org/ +Cortland,New York,23,36,36023,county,Cortland County,https://www.cortland-co.org/ +Coryell,Texas,99,48,48099,county,Coryell County,https://www.coryellcounty.org/ +Coshocton,Ohio,31,39,39031,county,Coshocton County,https://www.coshoctoncounty.net/ +Costilla,Colorado,23,8,8023,county,Costilla County,https://costillacounty.colorado.gov/ +Cottle,Texas,101,48,48101,county,Cottle County,https://www.co.cottle.tx.us/ +Cotton,Oklahoma,33,40,40033,county,Cotton County,https://oklahoma.gov/okdhs/library/resources/cottonrd211.html +Cottonwood,Minnesota,33,27,27033,county,Cottonwood County,https://www.co.cottonwood.mn.us/ +Covington,Alabama,39,1,1039,county,Covington County,http://www.covcounty.com/ +Covington,Mississippi,31,28,28031,county,Covington County,https://www.covingtoncountyms.gov/ +Covington,Virginia,580,51,51580,city,Covington city,https://covington.va.us/ +Coweta,Georgia,77,13,13077,county,Coweta County,https://www.coweta.ga.us/ +Cowley,Kansas,35,20,20035,county,Cowley County,https://www.cowleycountyks.gov/ +Cowlitz,Washington,15,53,53015,county,Cowlitz County,https://www.co.cowlitz.wa.us/ +Craig,Oklahoma,35,40,40035,county,Craig County,https://www.ok.gov/triton/modules/okmap/county_map.php?ac=175&okmap_id=36&county_seq=18 +Craig,Virginia,45,51,51045,county,Craig County,https://craigcountyva.gov/ +Craighead,Arkansas,31,5,5031,county,Craighead County,https://craigheadcountyar.gov/ +Crane,Texas,103,48,48103,county,Crane County,https://www.co.crane.tx.us/ +Craven,North Carolina,49,37,37049,county,Craven County,https://www.cravencountync.gov/ +Crawford,Pennsylvania,39,42,42039,county,Crawford County,https://www.crawfordcountypa.net/Pages/Home.aspx +Crawford,Arkansas,33,5,5033,county,Crawford County,https://www.crawford-county.org/ +Crawford,Illinois,33,17,17033,county,Crawford County,https://crawfordcountyil.org/ +Crawford,Indiana,25,18,18025,county,Crawford County,https://www.in.gov/core/mylocal/crawford_county.html +Crawford,Michigan,39,26,26039,county,Crawford County,https://www.crawfordco.org/ +Crawford,Missouri,55,29,29055,county,Crawford County,https://crawfordcountymo.net/ +Crawford,Iowa,47,19,19047,county,Crawford County,https://www.crawfordcounty.iowa.gov/ +Crawford,Kansas,37,20,20037,county,Crawford County,https://www.crawfordcountykansas.org/ +Crawford,Georgia,79,13,13079,county,Crawford County,https://www.crawfordcountyga.org/ +Crawford,Wisconsin,23,55,55023,county,Crawford County,https://www.crawfordcountywi.org/ +Crawford,Ohio,33,39,39033,county,Crawford County,https://crawford-co.org/ +Creek,Oklahoma,37,40,40037,county,Creek County,https://www.creekcountyonline.com/ +Crenshaw,Alabama,41,1,1041,county,Crenshaw County,https://crenshawcountyalonline.com/default.aspx?pageID=1 +Crisp,Georgia,81,13,13081,county,Crisp County,https://crispcounty.com/ +Crittenden,Kentucky,55,21,21055,county,Crittenden County,https://www.crittendencountyky.org/ +Crittenden,Arkansas,35,5,5035,county,Crittenden County,https://www.crittendencountyar.org/ +Crockett,Tennessee,33,47,47033,county,Crockett County,https://www.crockettvote.com/county-officials +Crockett,Texas,105,48,48105,county,Crockett County,https://www.co.crockett.tx.us/ +Crook,Wyoming,11,56,56011,county,Crook County,https://www.crookcounty.wy.gov/ +Crook,Oregon,13,41,41013,county,Crook County,https://co.crook.or.us/home +Crosby,Texas,107,48,48107,county,Crosby County,https://www.co.crosby.tx.us/ +Cross,Arkansas,37,5,5037,county,Cross County,https://www.crosscountyar.org/ +Crow Wing,Minnesota,35,27,27035,county,Crow Wing County,https://www.crowwing.gov/ +Crowley,Colorado,25,8,8025,county,Crowley County,https://crowleycounty.colorado.gov/ +Culberson,Texas,109,48,48109,county,Culberson County,https://www.co.culberson.tx.us/ +Cullman,Alabama,43,1,1043,county,Cullman County,http://www.co.cullman.al.us/ +Culpeper,Virginia,47,51,51047,county,Culpeper County,https://web.culpepercounty.gov/home +Cumberland,Tennessee,35,47,47035,county,Cumberland County,https://cumberlandcountytn.gov/ +Cumberland,Virginia,49,51,51049,county,Cumberland County,https://cumberlandcounty.virginia.gov/ +Cumberland,Illinois,35,17,17035,county,Cumberland County,https://cumberlandcoil.gov/ +Cumberland,New Jersey,11,34,34011,county,Cumberland County,https://www.cumberlandcountynj.gov/ +Cumberland,Maine,5,23,23005,county,Cumberland County,https://www.cumberlandcountyme.gov/ +Cumberland,North Carolina,51,37,37051,county,Cumberland County,https://www.cumberlandcountync.gov/ +Cumberland,Pennsylvania,41,42,42041,county,Cumberland County,https://www.cumberlandcountypa.gov/ +Cumberland,Kentucky,57,21,21057,county,Cumberland County,https://cumberlandcounty.ky.gov/ +Cuming,Nebraska,39,31,31039,county,Cuming County,https://cumingcountyne.gov/ +Currituck,North Carolina,53,37,37053,county,Currituck County,https://currituckcountync.gov/ +Curry,Oregon,15,41,41015,county,Curry County,https://www.co.curry.or.us/ +Curry,New Mexico,9,35,35009,county,Curry County,https://www.currycountynm.gov/ +Custer,Montana,17,30,30017,county,Custer County,https://custercountymt.gov/ +Custer,Idaho,37,16,16037,county,Custer County,https://custercountyidaho.org/ +Custer,Oklahoma,39,40,40039,county,Custer County,https://custer.okcounties.org/ +Custer,Colorado,27,8,8027,county,Custer County,https://www.custercounty-co.gov/ +Custer,Nebraska,41,31,31041,county,Custer County,https://custercountyne.gov/ +Custer,South Dakota,33,46,46033,county,Custer County,https://www.custercountysd.com/ +Cuyahoga,Ohio,35,39,39035,county,Cuyahoga County,https://cuyahogacounty.gov/ +Dade,Georgia,83,13,13083,county,Dade County,https://www.dadecounty-ga.gov/ +Dade,Missouri,57,29,29057,county,Dade County,https://www.dadecountymo.com/ +Daggett,Utah,9,49,49009,county,Daggett County,https://www.daggettcounty.org/ +Dakota,Nebraska,43,31,31043,county,Dakota County,https://dakotacountyne.org/ +Dakota,Minnesota,37,27,27037,county,Dakota County,https://www.co.dakota.mn.us/ +Dale,Alabama,45,1,1045,county,Dale County,https://dalecountyal.com/ +Dallam,Texas,111,48,48111,county,Dallam County,http://www.dallam.org/county/ +Dallas,Arkansas,39,5,5039,county,Dallas County,https://www.arcounties.org/counties/dallas/ +Dallas,Texas,113,48,48113,county,Dallas County,https://www.dallascounty.org/ +Dallas,Iowa,49,19,19049,county,Dallas County,https://www.dallascountyiowa.gov/ +Dallas,Missouri,59,29,29059,county,Dallas County,https://www.buffalococ.com/dallasgov.htm +Dallas,Alabama,47,1,1047,county,Dallas County,http://www.dallascounty-al.org/ +Dane,Wisconsin,25,55,55025,county,Dane County,https://www.countyofdane.com/ +Daniels,Montana,19,30,30019,county,Daniels County,https://www.scobeymt.com/ +Danville,Virginia,590,51,51590,city,Danville city,https://www.danville-va.gov/ +Dare,North Carolina,55,37,37055,county,Dare County,https://www.darenc.gov/ +Darke,Ohio,37,39,39037,county,Darke County,https://www.mydarkecounty.com/ +Darlington,South Carolina,31,45,45031,county,Darlington County,https://www.darcosc.com/ +Dauphin,Pennsylvania,43,42,42043,county,Dauphin County,http://www.dauphincounty.gov/ +Davidson,North Carolina,57,37,37057,county,Davidson County,https://www.co.davidson.nc.us/ +Davidson,Tennessee,37,47,47037,county,Davidson County,https://www.nashville.gov/ +Davie,North Carolina,59,37,37059,county,Davie County,https://www.daviecountync.gov/ +Daviess,Missouri,61,29,29061,county,Daviess County,https://www.daviesscountymo.gov/ +Daviess,Kentucky,59,21,21059,county,Daviess County,https://www.daviessky.org/ +Daviess,Indiana,27,18,18027,county,Daviess County,https://www.daviess.org/ +Davis,Iowa,51,19,19051,county,Davis County,https://www.daviscountyiowa.gov/ +Davis,Utah,11,49,49011,county,Davis County,https://www.daviscountyutah.gov/ +Davison,South Dakota,35,46,46035,county,Davison County,https://www.davisoncounty.org/ +Dawes,Nebraska,45,31,31045,county,Dawes County,https://dawes-county.com/ +Dawson,Nebraska,47,31,31047,county,Dawson County,https://www.dawsoncountyne.gov/ +Dawson,Texas,115,48,48115,county,Dawson County,https://www.co.dawson.tx.us/ +Dawson,Georgia,85,13,13085,county,Dawson County,https://www.dawsoncountyga.gov/home +Dawson,Montana,21,30,30021,county,Dawson County,https://www.dawsonmt.gov/ +Day,South Dakota,37,46,46037,county,Day County,https://day.sdcounties.org/ +De Baca,New Mexico,11,35,35011,county,De Baca County,https://www.nmcounties.org/counties/de-baca-county/ +De Soto,Louisiana,31,22,22031,parish,De Soto Parish,https://www.louisiana.gov/local-louisiana/desoto-parish +De Witt,Illinois,39,17,17039,county,De Witt County,https://www.dewittcountyill.com/ +DeKalb,Alabama,49,1,1049,county,DeKalb County,https://www.revenue-dekalbco-al.us/ +DeKalb,Illinois,37,17,17037,county,DeKalb County,https://dekalbcounty.org/ +DeKalb,Georgia,89,13,13089,county,DeKalb County,https://www.dekalbcountyga.gov/ +DeKalb,Missouri,63,29,29063,county,DeKalb County,https://www.dekalbcountymo.com/ +DeKalb,Indiana,33,18,18033,county,DeKalb County,https://www.co.dekalb.in.us/ +DeKalb,Tennessee,41,47,47041,county,DeKalb County,http://www.dekalbtennessee.com/ +DeSoto,Mississippi,33,28,28033,county,DeSoto County,https://www.desotocountyms.gov/ +DeSoto,Florida,27,12,12027,county,DeSoto County,https://desotobocc.com/ +DeWitt,Texas,123,48,48123,county,DeWitt County,https://www.co.dewitt.tx.us/ +Deaf Smith,Texas,117,48,48117,county,Deaf Smith County,https://www.co.deaf-smith.tx.us/ +Dearborn,Indiana,29,18,18029,county,Dearborn County,https://www.dearborncounty.org/ +Decatur,Kansas,39,20,20039,county,Decatur County,https://www.dccoks.org/ +Decatur,Indiana,31,18,18031,county,Decatur County,http://www.decaturcounty.in.gov/ +Decatur,Georgia,87,13,13087,county,Decatur County,https://www.decaturcountyga.gov/ +Decatur,Tennessee,39,47,47039,county,Decatur County,https://decaturcountytn.org/ +Decatur,Iowa,53,19,19053,county,Decatur County,https://www.decaturcountyiowa.gov/ +Deer Lodge,Montana,23,30,30023,county,Deer Lodge County,https://www.adlc.us/ +Defiance,Ohio,39,39,39039,county,Defiance County,https://www.defiance-county.com/ +Del Norte,California,15,6,6015,county,Del Norte County,https://www.co.del-norte.ca.us/ +Delaware,Ohio,41,39,39041,county,Delaware County,https://co.delaware.oh.us/ +Delaware,Pennsylvania,45,42,42045,county,Delaware County,https://delcopa.gov/ +Delaware,Indiana,35,18,18035,county,Delaware County,https://www.co.delaware.in.us/ +Delaware,Oklahoma,41,40,40041,county,Delaware County,https://delaware.okcounties.org/ +Delaware,Iowa,55,19,19055,county,Delaware County,https://delawarecounty.iowa.gov/ +Delaware,New York,25,36,36025,county,Delaware County,https://www.delcony.us/ +Delta,Colorado,29,8,8029,county,Delta County,https://www.deltacountyco.gov/ +Delta,Texas,119,48,48119,county,Delta County,https://www.deltacountytx.com/ +Delta,Michigan,41,26,26041,county,Delta County,https://deltacountymi.gov/ +Dent,Missouri,65,29,29065,county,Dent County,https://www.salemmo.com/dent/index.php +Denton,Texas,121,48,48121,county,Denton County,https://www.dentoncounty.gov/ +Denver,Colorado,31,8,8031,county,Denver County,https://www.denvergov.org/ +Des Moines,Iowa,57,19,19057,county,Des Moines County,https://desmoinescounty.iowa.gov/ +Deschutes,Oregon,17,41,41017,county,Deschutes County,https://www.deschutes.org/ +Desha,Arkansas,41,5,5041,county,Desha County,https://local.arkansas.gov/local.php?agency=Desha%20County +Deuel,Nebraska,49,31,31049,county,Deuel County,https://co.deuel.ne.us/ +Deuel,South Dakota,39,46,46039,county,Deuel County,https://www.deuelcountysd.com/ +Dewey,Oklahoma,43,40,40043,county,Dewey County,https://oklahoma.gov/okdhs/library/resources/deweyrd211.html +Dewey,South Dakota,41,46,46041,county,Dewey County,https://ujs.sd.gov/Fourth_Circuit/Links/Counties.aspx?P%2Bs%2FbRv%2BGei085%2B%2FR%2FRlXB5Sz4%2FMMNiU5eVx47cO1SE%3D +Dickens,Texas,125,48,48125,county,Dickens County,https://www.co.dickens.tx.us/ +Dickenson,Virginia,51,51,51051,county,Dickenson County,https://dickensonva.org/ +Dickey,North Dakota,21,38,38021,county,Dickey County,https://dickeynd.com/ +Dickinson,Michigan,43,26,26043,county,Dickinson County,https://www.dickinsoncountymi.gov/ +Dickinson,Kansas,41,20,20041,county,Dickinson County,https://www.dkcoks.gov/ +Dickinson,Iowa,59,19,19059,county,Dickinson County,https://dickinsoncountyiowa.gov/ +Dickson,Tennessee,43,47,47043,county,Dickson County,https://www.dicksoncountytn.gov/ +Dillon,South Carolina,33,45,45033,county,Dillon County,https://www.dilloncountysc.org/ +Dimmit,Texas,127,48,48127,county,Dimmit County,https://www.dimmitcounty.org/ +Dinwiddie,Virginia,53,51,51053,county,Dinwiddie County,https://www.dinwiddieva.us/ +District of Columbia,District of Columbia,1,11,11001,district,District of Columbia,https://dc.gov/ +Divide,North Dakota,23,38,38023,county,Divide County,https://www.dividecountynd.org/ +Dixie,Florida,29,12,12029,county,Dixie County,https://www.dixiecounty.us/ +Dixon,Nebraska,51,31,31051,county,Dixon County,https://dixoncountyne.gov/ +Doddridge,West Virginia,17,54,54017,county,Doddridge County,https://www.doddridgecountywv.gov/ +Dodge,Georgia,91,13,13091,county,Dodge County,https://dodgecountyga.com/ +Dodge,Wisconsin,27,55,55027,county,Dodge County,https://www.co.dodge.wi.gov/ +Dodge,Minnesota,39,27,27039,county,Dodge County,https://www.co.dodge.mn.us/ +Dodge,Nebraska,53,31,31053,county,Dodge County,https://dodgecounty.nebraska.gov/ +Dolores,Colorado,33,8,8033,county,Dolores County,https://dolocnty.colorado.gov/ +Doniphan,Kansas,43,20,20043,county,Doniphan County,http://dpcountyks.com/ +Donley,Texas,129,48,48129,county,Donley County,https://www.co.donley.tx.us/ +Dooly,Georgia,93,13,13093,county,Dooly County,https://doolycountyga.com/ +Door,Wisconsin,29,55,55029,county,Door County,https://www.doorcounty.com/ +Dorchester,Maryland,19,24,24019,county,Dorchester County,https://dorchestercountymd.com/ +Dorchester,South Carolina,35,45,45035,county,Dorchester County,https://www.dorchestercountysc.gov/ +Dougherty,Georgia,95,13,13095,county,Dougherty County,https://www.dougherty.ga.us/ +Douglas,Minnesota,41,27,27041,county,Douglas County,https://www.douglascountymn.gov/ +Douglas,Nevada,5,32,32005,county,Douglas County,https://www.douglascountynv.gov/ +Douglas,Missouri,67,29,29067,county,Douglas County,https://douglascountycollector.com/ +Douglas,Nebraska,55,31,31055,county,Douglas County,https://www.douglascounty-ne.gov/ +Douglas,Georgia,97,13,13097,county,Douglas County,https://www.celebratedouglascounty.com/ +Douglas,Wisconsin,31,55,55031,county,Douglas County,https://www.douglascountywi.org/ +Douglas,South Dakota,43,46,46043,county,Douglas County,https://douglas.sdcounties.org/ +Douglas,Washington,17,53,53017,county,Douglas County,https://www.douglascountywa.net/ +Douglas,Oregon,19,41,41019,county,Douglas County,https://douglascountyor.gov/ +Douglas,Kansas,45,20,20045,county,Douglas County,https://www.douglascountyks.org/ +Douglas,Colorado,35,8,8035,county,Douglas County,https://www.douglas.co.us/ +Douglas,Illinois,41,17,17041,county,Douglas County,https://douglascountyil.gov/ +Doña Ana,New Mexico,13,35,35013,county,Doña Ana County,https://www.donaanacounty.org/?locale=en +Drew,Arkansas,43,5,5043,county,Drew County,https://local.arkansas.gov/local.php?agency=Drew%20County +DuPage,Illinois,43,17,17043,county,DuPage County,https://www.dupagecounty.gov/ +Dubois,Indiana,37,18,18037,county,Dubois County,https://www.duboiscountyin.org/ +Dubuque,Iowa,61,19,19061,county,Dubuque County,https://www.dubuquecountyiowa.gov/ +Duchesne,Utah,13,49,49013,county,Duchesne County,https://duchesne.utah.gov/ +Dukes,Massachusetts,7,25,25007,county,Dukes County,https://www.dukescounty.org/ +Dundy,Nebraska,57,31,31057,county,Dundy County,https://dundycounty.nebraska.gov/ +Dunklin,Missouri,69,29,29069,county,Dunklin County,https://dunklincounty.org/ +Dunn,North Dakota,25,38,38025,county,Dunn County,https://www.dunncountynd.org/ +Dunn,Wisconsin,33,55,55033,county,Dunn County,https://dunncountywi.gov/ +Duplin,North Carolina,61,37,37061,county,Duplin County,https://www.duplincountync.com/ +Durham,North Carolina,63,37,37063,county,Durham County,https://www.dconc.gov/ +Dutchess,New York,27,36,36027,county,Dutchess County,https://www.dutchessny.gov/ +Duval,Florida,31,12,12031,county,Duval County,https://www.coj.net/ +Duval,Texas,131,48,48131,county,Duval County,https://www.co.duval.tx.us/ +Dyer,Tennessee,45,47,47045,county,Dyer County,https://www.dyercounty.com/ +Eagle,Colorado,37,8,8037,county,Eagle County,https://eaglecounty.us/ +Early,Georgia,99,13,13099,county,Early County,https://www.earlycountyga.org/ +East Baton Rouge,Louisiana,33,22,22033,parish,East Baton Rouge Parish,https://www.louisiana.gov/local-louisiana/east-baton-rouge-parish +East Carroll,Louisiana,35,22,22035,parish,East Carroll Parish,https://www.louisiana.gov/local-louisiana/east-carroll-parish +East Feliciana,Louisiana,37,22,22037,parish,East Feliciana Parish,https://efparish.org/ +Eastland,Texas,133,48,48133,county,Eastland County,https://www.eastlandcountytexas.com/ +Eaton,Michigan,45,26,26045,county,Eaton County,https://www.eatoncounty.org/ +Eau Claire,Wisconsin,35,55,55035,county,Eau Claire County,https://www.eauclairecounty.gov/ +Echols,Georgia,101,13,13101,county,Echols County,https://echolscountyga.com/ +Ector,Texas,135,48,48135,county,Ector County,https://www.co.ector.tx.us/ +Eddy,New Mexico,15,35,35015,county,Eddy County,https://www.co.eddy.nm.us/ +Eddy,North Dakota,27,38,38027,county,Eddy County,https://www.eddycountynd.org/ +Edgar,Illinois,45,17,17045,county,Edgar County,https://edgarcountyillinois.com/ +Edgecombe,North Carolina,65,37,37065,county,Edgecombe County,https://www.edgecombecountync.gov/ +Edgefield,South Carolina,37,45,45037,county,Edgefield County,https://edgefieldcounty.sc.gov/ +Edmonson,Kentucky,61,21,21061,county,Edmonson County,https://edmonsoncounty.ky.gov/Pages/index.aspx +Edmunds,South Dakota,45,46,46045,county,Edmunds County,https://edmunds.sdcounties.org/ +Edwards,Kansas,47,20,20047,county,Edwards County,https://kansastreasurers.org/index.php/directory/edwards-county/ +Edwards,Texas,137,48,48137,county,Edwards County,https://www.co.edwards.tx.us/ +Edwards,Illinois,47,17,17047,county,Edwards County,https://www.ilsos.gov/departments/archives/IRAD/edwards.html +Effingham,Georgia,103,13,13103,county,Effingham County,https://www.effinghamcounty.org/ +Effingham,Illinois,49,17,17049,county,Effingham County,http://www.co.effingham.il.us/ +El Dorado,California,17,6,6017,county,El Dorado County,https://www.edcgov.us/ +El Paso,Texas,141,48,48141,county,El Paso County,http://www.epcounty.com/ +El Paso,Colorado,41,8,8041,county,El Paso County,https://www.elpasoco.com/ +Elbert,Colorado,39,8,8039,county,Elbert County,https://www.elbertcounty-co.gov/ +Elbert,Georgia,105,13,13105,county,Elbert County,https://www.elbertga.us/ +Elk,Kansas,49,20,20049,county,Elk County,https://elkcountyks.org/ +Elk,Pennsylvania,47,42,42047,county,Elk County,https://www.co.elk.pa.us/ +Elkhart,Indiana,39,18,18039,county,Elkhart County,https://elkhartcounty.com/ +Elko,Nevada,7,32,32007,county,Elko County,https://www.elkocountynv.net/ +Elliott,Kentucky,63,21,21063,county,Elliott County,https://elliottcounty.ky.gov/ +Ellis,Texas,139,48,48139,county,Ellis County,https://www.co.ellis.tx.us/ +Ellis,Oklahoma,45,40,40045,county,Ellis County,https://oklahoma.gov/okdhs/library/resources/ellisrd211.html +Ellis,Kansas,51,20,20051,county,Ellis County,https://www.ellisco.net/ +Ellsworth,Kansas,53,20,20053,county,Ellsworth County,http://www.ellsworthcounty.org/ +Elmore,Idaho,39,16,16039,county,Elmore County,https://elmorecounty.org/ +Elmore,Alabama,51,1,1051,county,Elmore County,https://www.elmoreco.org/ +Emanuel,Georgia,107,13,13107,county,Emanuel County,https://www.emanuelco-ga.gov/ +Emery,Utah,15,49,49015,county,Emery County,https://emerycounty.com/ +Emmet,Michigan,47,26,26047,county,Emmet County,https://www.emmetcounty.org/ +Emmet,Iowa,63,19,19063,county,Emmet County,https://emmetcounty.iowa.gov/ +Emmons,North Dakota,29,38,38029,county,Emmons County,http://www.emmonsnd.com/ +Emporia,Virginia,595,51,51595,city,Emporia city,https://www.ci.emporia.va.us/ +Erath,Texas,143,48,48143,county,Erath County,https://co.erath.tx.us/ +Erie,Pennsylvania,49,42,42049,county,Erie County,https://eriecountypa.gov/ +Erie,New York,29,36,36029,county,Erie County,https://www.erie.gov/ +Erie,Ohio,43,39,39043,county,Erie County,https://www.eriecounty.oh.gov/ +Escambia,Florida,33,12,12033,county,Escambia County,https://myescambia.com/ +Escambia,Alabama,53,1,1053,county,Escambia County,https://www.escambiacountyal.gov/ +Esmeralda,Nevada,9,32,32009,county,Esmeralda County,https://www.accessesmeralda.com/ +Essex,Vermont,9,50,50009,county,Essex County,https://www.essexvt.org/ +Essex,Virginia,57,51,51057,county,Essex County,https://www.essex-virginia.org/home +Essex,New York,31,36,36031,county,Essex County,https://essexcountyny.gov/ +Essex,New Jersey,13,34,34013,county,Essex County,http://essexcountynj.org/ +Essex,Massachusetts,9,25,25009,county,Essex County,https://www.mass.gov/locations/essex-county-superior-court +Estill,Kentucky,65,21,21065,county,Estill County,https://estillky.com/ +Etowah,Alabama,55,1,1055,county,Etowah County,https://etowahcounty.org/ +Eureka,Nevada,11,32,32011,county,Eureka County,https://www.co.eureka.nv.us/ +Evangeline,Louisiana,39,22,22039,parish,Evangeline Parish,https://www.louisiana.gov/local-louisiana/evangeline-parish +Evans,Georgia,109,13,13109,county,Evans County,https://evanscounty.org/ +Fairfax City,Virginia,600,51,51600,city,Fairfax city,https://www.fairfaxva.gov/ +Fairfax,Virginia,59,51,51059,county,Fairfax County,https://www.fairfaxcounty.gov/ +Fairfield,South Carolina,39,45,45039,county,Fairfield County,https://www.fairfieldsc.com/ +Fairfield,Ohio,45,39,39045,county,Fairfield County,https://www.co.fairfield.oh.us/ +Fall River,South Dakota,47,46,46047,county,Fall River County,https://fallriver.sdcounties.org/ +Fallon,Montana,25,30,30025,county,Fallon County,https://www.falloncounty.net/ +Falls,Texas,145,48,48145,county,Falls County,https://www.co.falls.tx.us/ +Falls Church,Virginia,610,51,51610,city,Falls Church city,https://www.fallschurchva.gov/ +Fannin,Texas,147,48,48147,county,Fannin County,https://www.co.fannin.tx.us/ +Fannin,Georgia,111,13,13111,county,Fannin County,https://www.fannincountyga.com/ +Faribault,Minnesota,43,27,27043,county,Faribault County,https://www.co.faribault.mn.us/ +Faulk,South Dakota,49,46,46049,county,Faulk County,https://ujs.sd.gov/Fifth_Circuit/Links/Counties.aspx?5hAfdUMere99rbewDIQHjV39z0OW514fAZBbN%2FygiKw%3D +Faulkner,Arkansas,45,5,5045,county,Faulkner County,https://www.faulknercounty.org/ +Fauquier,Virginia,61,51,51061,county,Fauquier County,https://www.fauquiercounty.gov/?locale=en +Fayette,Tennessee,47,47,47047,county,Fayette County,https://fayettetn.us/ +Fayette,Illinois,51,17,17051,county,Fayette County,https://www.fayettecountyillinois.gov/ +Fayette,Ohio,47,39,39047,county,Fayette County,https://www.fayette-co-oh.com/ +Fayette,Iowa,65,19,19065,county,Fayette County,https://fayettecounty.iowa.gov/ +Fayette,Georgia,113,13,13113,county,Fayette County,https://fayettecountyga.gov/ +Fayette,Indiana,41,18,18041,county,Fayette County,https://www.in.gov/core/mylocal/fayette_county.html +Fayette,Texas,149,48,48149,county,Fayette County,https://www.co.fayette.tx.us/ +Fayette,Pennsylvania,51,42,42051,county,Fayette County,https://www.fayettecountypa.org/ +Fayette,Alabama,57,1,1057,county,Fayette County,https://fayetteal.org/ +Fayette,Kentucky,67,21,21067,county,Fayette County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Fayette+County +Fayette,West Virginia,19,54,54019,county,Fayette County,https://fayettecounty.wv.gov/ +Fentress,Tennessee,49,47,47049,county,Fentress County,https://fentresscountytn.gov/ +Fergus,Montana,27,30,30027,county,Fergus County,https://co.fergus.mt.us/ +Ferry,Washington,19,53,53019,county,Ferry County,https://www.ferry-county.com/ +Fillmore,Nebraska,59,31,31059,county,Fillmore County,https://fillmorecountyne.gov/ +Fillmore,Minnesota,45,27,27045,county,Fillmore County,https://www.co.fillmore.mn.us/ +Finney,Kansas,55,20,20055,county,Finney County,https://www.finneycounty.org/ +Fisher,Texas,151,48,48151,county,Fisher County,https://www.fishercounty.org/ +Flagler,Florida,35,12,12035,county,Flagler County,https://www.flaglercounty.gov/ +Flathead,Montana,29,30,30029,county,Flathead County,https://flathead.mt.gov/ +Fleming,Kentucky,69,21,21069,county,Fleming County,http://www.flemingcountyky.us/ +Florence,South Carolina,41,45,45041,county,Florence County,http://florenceco.org/ +Florence,Wisconsin,37,55,55037,county,Florence County,https://www.florencecountywi.com/ +Floyd,Indiana,43,18,18043,county,Floyd County,https://www.floydcounty.in.gov/ +Floyd,Georgia,115,13,13115,county,Floyd County,https://www.floydcountyga.gov/home +Floyd,Iowa,67,19,19067,county,Floyd County,https://www.floydco.iowa.gov/ +Floyd,Virginia,63,51,51063,county,Floyd County,https://www.floydcova.gov/ +Floyd,Texas,153,48,48153,county,Floyd County,https://www.co.floyd.tx.us/ +Floyd,Kentucky,71,21,21071,county,Floyd County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Floyd+County +Fluvanna,Virginia,65,51,51065,county,Fluvanna County,https://www.fluvannacounty.org/home +Foard,Texas,155,48,48155,county,Foard County,https://www.foardcounty.texas.gov/ +Fond du Lac,Wisconsin,39,55,55039,county,Fond du Lac County,https://www.fdlco.wi.gov/ +Ford,Illinois,53,17,17053,county,Ford County,https://fordcounty.illinois.gov/ +Ford,Kansas,57,20,20057,county,Ford County,https://www.fordcounty.net/ +Forest,Pennsylvania,53,42,42053,county,Forest County,https://www.co.forest.pa.us/ +Forest,Wisconsin,41,55,55041,county,Forest County,http://www.co.forest.wi.gov/ +Forrest,Mississippi,35,28,28035,county,Forrest County,https://forrestcountyms.us/ +Forsyth,North Carolina,67,37,37067,county,Forsyth County,https://www.co.forsyth.nc.us/ +Forsyth,Georgia,117,13,13117,county,Forsyth County,https://www.forsythco.com/ +Fort Bend,Texas,157,48,48157,county,Fort Bend County,https://www.fortbendcountytx.gov/ +Foster,North Dakota,31,38,38031,county,Foster County,https://fostercounty.com/ +Fountain,Indiana,45,18,18045,county,Fountain County,https://www.fountaincounty.net/ +Franklin,Missouri,71,29,29071,county,Franklin County,https://www.franklinmo.org/ +Franklin,Florida,37,12,12037,county,Franklin County,https://www.franklincountyflorida.com/ +Franklin City,Virginia,620,51,51620,city,Franklin city,https://www.franklinva.com/ +Franklin,Illinois,55,17,17055,county,Franklin County,https://franklincountyil.gov/ +Franklin,Georgia,119,13,13119,county,Franklin County,https://www.franklincountyga.gov/ +Franklin,Texas,159,48,48159,county,Franklin County,https://www.co.franklin.tx.us/ +Franklin,Iowa,69,19,19069,county,Franklin County,https://www.franklincountyia.gov/ +Franklin,Nebraska,61,31,31061,county,Franklin County,https://franklincountyne.gov/ +Franklin,Idaho,41,16,16041,county,Franklin County,https://www.franklincountyidaho.org/ +Franklin,Pennsylvania,55,42,42055,county,Franklin County,https://franklincountypa.gov/ +Franklin,Ohio,49,39,39049,county,Franklin County,https://www.franklincountyohio.gov/ +Franklin,Virginia,67,51,51067,county,Franklin County,https://www.franklincountyva.gov/ +Franklin,Tennessee,51,47,47051,county,Franklin County,https://franklincotn.us/ +Franklin,Vermont,11,50,50011,county,Franklin County,http://bgs.vermont.gov/facilities/facilitiesbydistrict/franklincountycourt +Franklin,Louisiana,41,22,22041,parish,Franklin Parish,https://www.louisiana.gov/local-louisiana/franklin-parish +Franklin,Alabama,59,1,1059,county,Franklin County,http://www.franklincountyal.org/ +Franklin,Massachusetts,11,25,25011,county,Franklin County,https://www.mass.gov/locations/franklin-county-superior-court +Franklin,Mississippi,37,28,28037,county,Franklin County,https://franklincoms.weebly.com/ +Franklin,Arkansas,47,5,5047,county,Franklin County,https://local.arkansas.gov/local.php?agency=Franklin%20County +Franklin,New York,33,36,36033,county,Franklin County,https://www.franklincountyny.gov/ +Franklin,Maine,7,23,23007,county,Franklin County,https://www.franklincounty.maine.gov/ +Franklin,Indiana,47,18,18047,county,Franklin County,https://www.franklincounty.in.gov/ +Franklin,Washington,21,53,53021,county,Franklin County,https://www.franklincountywa.gov/ +Franklin,Kentucky,73,21,21073,county,Franklin County,https://franklincounty.ky.gov/ +Franklin,Kansas,59,20,20059,county,Franklin County,https://www.franklincoks.org/ +Franklin,North Carolina,69,37,37069,county,Franklin County,https://www.franklincountync.gov/ +Frederick,Maryland,21,24,24021,county,Frederick County,https://frederickcountymd.gov/ +Frederick,Virginia,69,51,51069,county,Frederick County,https://www.fcva.us/ +Fredericksburg,Virginia,630,51,51630,city,Fredericksburg city,https://www.fredericksburgva.gov/ +Freeborn,Minnesota,47,27,27047,county,Freeborn County,https://www.co.freeborn.mn.us/ +Freestone,Texas,161,48,48161,county,Freestone County,https://www.co.freestone.tx.us/ +Fremont,Iowa,71,19,19071,county,Fremont County,https://www.fremontcountyia.gov/ +Fremont,Colorado,43,8,8043,county,Fremont County,https://www.fremontco.com/ +Fremont,Idaho,43,16,16043,county,Fremont County,https://www.co.fremont.id.us/ +Fremont,Wyoming,13,56,56013,county,Fremont County,https://www.fremontcountywy.org/ +Fresno,California,19,6,6019,county,Fresno County,https://www.fresnocountyca.gov/Home +Frio,Texas,163,48,48163,county,Frio County,https://www.co.frio.tx.us/ +Frontier,Nebraska,63,31,31063,county,Frontier County,https://co.frontier.ne.us/ +Fulton,Ohio,51,39,39051,county,Fulton County,https://www.fultoncountyoh.com/ +Fulton,Pennsylvania,57,42,42057,county,Fulton County,https://www.co.fulton.pa.us/ +Fulton,Indiana,49,18,18049,county,Fulton County,https://www.co.fulton.in.us/ +Fulton,Arkansas,49,5,5049,county,Fulton County,https://www.fultoncountyar.gov/ +Fulton,Illinois,57,17,17057,county,Fulton County,https://www.fultonco.org/ +Fulton,Kentucky,75,21,21075,county,Fulton County,https://fultoncounty.ky.gov/ +Fulton,Georgia,121,13,13121,county,Fulton County,https://www.fultoncountyga.gov/ +Fulton,New York,35,36,36035,county,Fulton County,https://www.fultoncountyny.gov/home-fulton-county +Furnas,Nebraska,65,31,31065,county,Furnas County,https://furnascounty.ne.gov/ +Gadsden,Florida,39,12,12039,county,Gadsden County,https://www.gadsdencountyfl.gov/ +Gage,Nebraska,67,31,31067,county,Gage County,https://gagecountyne.gov/ +Gaines,Texas,165,48,48165,county,Gaines County,https://www.co.gaines.tx.us/ +Galax,Virginia,640,51,51640,city,Galax city,https://galaxva.com/ +Gallatin,Kentucky,77,21,21077,county,Gallatin County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Gallatin+County +Gallatin,Illinois,59,17,17059,county,Gallatin County,https://gallatinco.illinois.gov/ +Gallatin,Montana,31,30,30031,county,Gallatin County,https://gallatincomt.virtualtownhall.net/ +Gallia,Ohio,53,39,39053,county,Gallia County,https://www.gallianet.net/ +Galveston,Texas,167,48,48167,county,Galveston County,https://www.galvestoncountytx.gov/ +Garden,Nebraska,69,31,31069,county,Garden County,https://gardencounty.ne.gov/ +Garfield,Montana,33,30,30033,county,Garfield County,https://garfieldco.us/ +Garfield,Nebraska,71,31,31071,county,Garfield County,https://garfieldcounty.ne.gov/ +Garfield,Oklahoma,47,40,40047,county,Garfield County,https://www.garfieldok.com/ +Garfield,Utah,17,49,49017,county,Garfield County,https://www.garfield.utah.gov/ +Garfield,Washington,23,53,53023,county,Garfield County,https://www.co.garfield.wa.us/home +Garfield,Colorado,45,8,8045,county,Garfield County,https://www.garfield-county.com/ +Garland,Arkansas,51,5,5051,county,Garland County,https://www.garlandcounty.org/ +Garrard,Kentucky,79,21,21079,county,Garrard County,https://garrardcountyky.gov/ +Garrett,Maryland,23,24,24023,county,Garrett County,https://www.garrettcounty.org/ +Garvin,Oklahoma,49,40,40049,county,Garvin County,https://oklahoma.gov/okdhs/library/resources/garvinrd211.html +Garza,Texas,169,48,48169,county,Garza County,https://www.garzacounty.net/ +Gasconade,Missouri,73,29,29073,county,Gasconade County,https://gasconadecounty.org/ +Gaston,North Carolina,71,37,37071,county,Gaston County,https://www.gastongov.com/ +Gates,North Carolina,73,37,37073,county,Gates County,https://gatescountync.gov/ +Geary,Kansas,61,20,20061,county,Geary County,https://www.gearycounty.org/ +Geauga,Ohio,55,39,39055,county,Geauga County,https://co.geauga.oh.us/ +Gem,Idaho,45,16,16045,county,Gem County,https://www.gemcounty.org/ +Genesee,Michigan,49,26,26049,county,Genesee County,https://www.geneseecountymi.gov/ +Genesee,New York,37,36,36037,county,Genesee County,https://www.co.genesee.ny.us/ +Geneva,Alabama,61,1,1061,county,Geneva County,https://genevacountyal.gov/ +Gentry,Missouri,75,29,29075,county,Gentry County,https://gentrycounty.net/ +George,Mississippi,39,28,28039,county,George County,http://www.georgecountyms.com/ +Georgetown,South Carolina,43,45,45043,county,Georgetown County,https://www.gtcounty.org/ +Gibson,Tennessee,53,47,47053,county,Gibson County,https://gibsoncounty-tn.com/ +Gibson,Indiana,51,18,18051,county,Gibson County,http://gibsoncounty-in.gov/ +Gila,Arizona,7,4,4007,county,Gila County,https://www.gilacountyaz.gov/ +Gilchrist,Florida,41,12,12041,county,Gilchrist County,https://gilchrist.fl.us/ +Giles,Virginia,71,51,51071,county,Giles County,https://virginiasmtnplayground.com/ +Giles,Tennessee,55,47,47055,county,Giles County,https://gilescountytn.gov/ +Gillespie,Texas,171,48,48171,county,Gillespie County,https://www.gillespiecounty.org/ +Gilliam,Oregon,21,41,41021,county,Gilliam County,https://www.co.gilliam.or.us/ +Gilmer,Georgia,123,13,13123,county,Gilmer County,https://gilmercounty-ga.gov/ +Gilmer,West Virginia,21,54,54021,county,Gilmer County,https://gilmercounty.wv.gov/ +Gilpin,Colorado,47,8,8047,county,Gilpin County,https://gilpincounty.colorado.gov/ +Glacier,Montana,35,30,30035,county,Glacier County,https://www.mtcounties.org/counties/county-websites/ +Glades,Florida,43,12,12043,county,Glades County,https://www.myglades.com/ +Gladwin,Michigan,51,26,26051,county,Gladwin County,https://gladwincounty-mi.gov/ +Glascock,Georgia,125,13,13125,county,Glascock County,http://glascockcountyga.com/ +Glasscock,Texas,173,48,48173,county,Glasscock County,https://www.co.glasscock.tx.us/ +Glenn,California,21,6,6021,county,Glenn County,https://www.countyofglenn.net/ +Gloucester,Virginia,73,51,51073,county,Gloucester County,https://gloucesterva.gov/ +Gloucester,New Jersey,15,34,34015,county,Gloucester County,https://www.gloucestercountynj.gov/ +Glynn,Georgia,127,13,13127,county,Glynn County,https://www.glynncounty.org/ +Gogebic,Michigan,53,26,26053,county,Gogebic County,https://www.gogebiccountymi.gov/ +Golden Valley,Montana,37,30,30037,county,Golden Valley County,https://www.goldenvalleycountymt.org/ +Golden Valley,North Dakota,33,38,38033,county,Golden Valley County,https://www.goldenvalleycounty.org/ +Goliad,Texas,175,48,48175,county,Goliad County,https://www.co.goliad.tx.us/ +Gonzales,Texas,177,48,48177,county,Gonzales County,https://www.co.gonzales.tx.us/ +Goochland,Virginia,75,51,51075,county,Goochland County,https://www.goochlandva.us/ +Goodhue,Minnesota,49,27,27049,county,Goodhue County,https://co.goodhue.mn.us/ +Gooding,Idaho,47,16,16047,county,Gooding County,https://www.goodingcounty.org/ +Gordon,Georgia,129,13,13129,county,Gordon County,https://gordoncounty.org/ +Goshen,Wyoming,15,56,56015,county,Goshen County,https://goshencounty.org/ +Gosper,Nebraska,73,31,31073,county,Gosper County,https://gospercountyne.gov/ +Gove,Kansas,63,20,20063,county,Gove County,https://govecountyks.gov/ +Grady,Oklahoma,51,40,40051,county,Grady County,https://oklahoma.gov/health/locations/county-health-departments/grady-county-health-department.html +Grady,Georgia,131,13,13131,county,Grady County,https://gradycountyga.gov/ +Grafton,New Hampshire,9,33,33009,county,Grafton County,https://www.co.grafton.nh.us/ +Graham,Kansas,65,20,20065,county,Graham County,https://www.grahamcountyks.com/ +Graham,Arizona,9,4,4009,county,Graham County,https://www.graham.az.gov/ +Graham,North Carolina,75,37,37075,county,Graham County,https://www.grahamcounty.org/ +Grainger,Tennessee,57,47,47057,county,Grainger County,https://www.graingercountytn.com/ +Grand,Utah,19,49,49019,county,Grand County,https://www.grandcountyutah.net/ +Grand,Colorado,49,8,8049,county,Grand County,https://co.grand.co.us/ +Grand Forks,North Dakota,35,38,38035,county,Grand Forks County,https://www.gfcounty.nd.gov/ +Grand Isle,Vermont,13,50,50013,county,Grand Isle County,https://grandislevt.org/ +Grand Traverse,Michigan,55,26,26055,county,Grand Traverse County,https://www.gtcountymi.gov/ +Granite,Montana,39,30,30039,county,Granite County,https://www.granitecountymt.us/ +Grant,North Dakota,37,38,38037,county,Grant County,https://www.grantcountynd.com/ +Grant,Indiana,53,18,18053,county,Grant County,https://www.in.gov/core/mylocal/grant_county.html +Grant,West Virginia,23,54,54023,county,Grant County,https://www.grantcountywv.org/ +Grant,Arkansas,53,5,5053,county,Grant County,http://www.grantcountyar.com/ +Grant,Kentucky,81,21,21081,county,Grant County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Grant+County +Grant,Washington,25,53,53025,county,Grant County,https://www.grantcountywa.gov/ +Grant,Nebraska,75,31,31075,county,Grant County,https://grantcountynebraska.us/ +Grant,New Mexico,17,35,35017,county,Grant County,https://grantcountynm.gov/ +Grant,Louisiana,43,22,22043,parish,Grant Parish,https://www.louisiana.gov/local-louisiana/grant-parish +Grant,Oregon,23,41,41023,county,Grant County,https://grantcountyoregon.net/ +Grant,Oklahoma,53,40,40053,county,Grant County,https://grant.okcounties.org/ +Grant,South Dakota,51,46,46051,county,Grant County,https://electionresults.sd.gov/ResultsSW.aspx?type=CTYALL&cty=29&map=CTY +Grant,Wisconsin,43,55,55043,county,Grant County,http://www.co.grant.wi.gov/ +Grant,Minnesota,51,27,27051,county,Grant County,https://www.co.grant.mn.us/ +Grant,Kansas,67,20,20067,county,Grant County,https://www.grantcoks.org/ +Granville,North Carolina,77,37,37077,county,Granville County,https://www.granvillecounty.org/ +Gratiot,Michigan,57,26,26057,county,Gratiot County,https://www.gratiotmi.com/ +Graves,Kentucky,83,21,21083,county,Graves County,https://gravescounty.ky.gov/Pages/default.aspx +Gray,Texas,179,48,48179,county,Gray County,https://www.co.gray.tx.us/ +Gray,Kansas,69,20,20069,county,Gray County,http://www.gray.kansasgov.com/parcel/ +Grays Harbor,Washington,27,53,53027,county,Grays Harbor County,https://www.graysharbor.us/ +Grayson,Virginia,77,51,51077,county,Grayson County,https://www.graysoncountyva.gov/ +Grayson,Texas,181,48,48181,county,Grayson County,https://www.co.grayson.tx.us/ +Grayson,Kentucky,85,21,21085,county,Grayson County,https://www.graysoncountyky.gov/ +Greater Bridgeport,Connecticut,120,9,9120,planning region,Greater Bridgeport Planning Region,"https://en.wikipedia.org/wiki/Greater_Bridgeport_Planning_Region,_Connecticut" +Greeley,Kansas,71,20,20071,county,Greeley County,http://greeleycounty.org/ +Greeley,Nebraska,77,31,31077,county,Greeley County,https://greeleycounty.ne.gov/ +Green,Kentucky,87,21,21087,county,Green County,https://greencounty.ky.gov/Pages/index.aspx +Green,Wisconsin,45,55,55045,county,Green County,http://www.co.green.wi.gov/ +Green Lake,Wisconsin,47,55,55047,county,Green Lake County,https://www.greenlakecountywi.gov/ +Greenbrier,West Virginia,25,54,54025,county,Greenbrier County,https://greenbriercounty.net/ +Greene,Ohio,57,39,39057,county,Greene County,https://www.greenecountyohio.gov/ +Greene,Pennsylvania,59,42,42059,county,Greene County,https://www.co.greene.pa.us/ +Greene,New York,39,36,36039,county,Greene County,https://www.greenegovernment.com/ +Greene,Tennessee,59,47,47059,county,Greene County,https://www.greenecountytngov.com/ +Greene,North Carolina,79,37,37079,county,Greene County,https://greenecountync.gov/ +Greene,Indiana,55,18,18055,county,Greene County,https://www.co.greene.in.us/ +Greene,Arkansas,55,5,5055,county,Greene County,https://www.greenecounty.arkansas.gov/ +Greene,Missouri,77,29,29077,county,Greene County,https://greenecountymo.gov/ +Greene,Alabama,63,1,1063,county,Greene County,https://greenecountycommission.com/ +Greene,Illinois,61,17,17061,county,Greene County,https://www.ilsos.gov/departments/archives/IRAD/greene.html +Greene,Georgia,133,13,13133,county,Greene County,https://www.greenecountyga.gov/ +Greene,Mississippi,41,28,28041,county,Greene County,http://www.greenecountyms.gov/ +Greene,Iowa,73,19,19073,county,Greene County,https://www.co.greene.ia.us/ +Greene,Virginia,79,51,51079,county,Greene County,https://www.greenecountyva.gov/ +Greenlee,Arizona,11,4,4011,county,Greenlee County,https://www.co.greenlee.az.us/ +Greensville,Virginia,81,51,51081,county,Greensville County,https://www.greensvillecountyva.gov/ +Greenup,Kentucky,89,21,21089,county,Greenup County,https://www.greenupcountyky.gov/ +Greenville,South Carolina,45,45,45045,county,Greenville County,https://www.greenvillecounty.org/ +Greenwood,South Carolina,47,45,45047,county,Greenwood County,https://www.greenwoodcounty-sc.gov/ +Greenwood,Kansas,73,20,20073,county,Greenwood County,https://www.greenwoodcounty.org/ +Greer,Oklahoma,55,40,40055,county,Greer County,https://greer.okcounties.org/ +Gregg,Texas,183,48,48183,county,Gregg County,https://www.co.gregg.tx.us/ +Gregory,South Dakota,53,46,46053,county,Gregory County,https://gregory.sdcounties.org/ +Grenada,Mississippi,43,28,28043,county,Grenada County,http://www.grenadacountysheriff.org/page.php?id=5 +Griggs,North Dakota,39,38,38039,county,Griggs County,https://www.griggscountynd.gov/ +Grimes,Texas,185,48,48185,county,Grimes County,http://grimescountytexas.gov/ +Grundy,Tennessee,61,47,47061,county,Grundy County,https://www.grundycountytn.net/ +Grundy,Illinois,63,17,17063,county,Grundy County,https://www.grundycountyil.gov/ +Grundy,Missouri,79,29,29079,county,Grundy County,https://grundycountymo.com/wordpress/ +Grundy,Iowa,75,19,19075,county,Grundy County,https://www.grundycountyiowa.gov/ +Guadalupe,Texas,187,48,48187,county,Guadalupe County,https://www.co.guadalupe.tx.us/ +Guadalupe,New Mexico,19,35,35019,county,Guadalupe County,https://www.guadalupecountynm.org/ +Guernsey,Ohio,59,39,39059,county,Guernsey County,https://guernseycounty.org/ +Guilford,North Carolina,81,37,37081,county,Guilford County,https://www.guilfordcountync.gov/ +Gulf,Florida,45,12,12045,county,Gulf County,https://www.gulfcounty-fl.gov/ +Gunnison,Colorado,51,8,8051,county,Gunnison County,https://www.gunnisoncounty.org/ +Guthrie,Iowa,77,19,19077,county,Guthrie County,https://guthriecounty.gov/ +Gwinnett,Georgia,135,13,13135,county,Gwinnett County,https://www.gwinnettcounty.com/ +Haakon,South Dakota,55,46,46055,county,Haakon County,https://haakon.southdakotadirectors.com/ +Habersham,Georgia,137,13,13137,county,Habersham County,https://www.habershamga.com/ +Hale,Alabama,65,1,1065,county,Hale County,http://www.halecountyal.com/ +Hale,Texas,189,48,48189,county,Hale County,https://www.halecounty.org/ +Halifax,North Carolina,83,37,37083,county,Halifax County,https://www.halifaxnc.com/ +Halifax,Virginia,83,51,51083,county,Halifax County,https://www.halifaxcountyva.gov/ +Hall,Texas,191,48,48191,county,Hall County,https://www.co.hall.tx.us/ +Hall,Nebraska,79,31,31079,county,Hall County,https://www.hallcountyne.gov/ +Hall,Georgia,139,13,13139,county,Hall County,https://www.hallcounty.org/ +Hamblen,Tennessee,63,47,47063,county,Hamblen County,https://www.hamblencountytn.gov/ +Hamilton,Illinois,65,17,17065,county,Hamilton County,https://www.hamiltoncountyil.gov/ +Hamilton,Nebraska,81,31,31081,county,Hamilton County,https://hamiltoncountyne.gov/ +Hamilton,Tennessee,65,47,47065,county,Hamilton County,https://www.hamiltontn.gov/ +Hamilton,New York,41,36,36041,county,Hamilton County,https://www.hamiltoncounty.com/ +Hamilton,Ohio,61,39,39061,county,Hamilton County,https://www.hamiltoncountyohio.gov/ +Hamilton,Texas,193,48,48193,county,Hamilton County,https://www.co.hamilton.tx.us/ +Hamilton,Kansas,75,20,20075,county,Hamilton County,https://hamiltoncountyks.com/ +Hamilton,Florida,47,12,12047,county,Hamilton County,https://hamiltoncountyfl.com/ +Hamilton,Iowa,79,19,19079,county,Hamilton County,https://www.hamiltoncounty.iowa.gov/ +Hamilton,Indiana,57,18,18057,county,Hamilton County,https://www.hamiltoncounty.in.gov/ +Hamlin,South Dakota,57,46,46057,county,Hamlin County,https://www.hamlincountysd.org/ +Hampden,Massachusetts,13,25,25013,county,Hampden County,https://www.hampdenma.gov/ +Hampshire,Massachusetts,15,25,25015,county,Hampshire County,https://www.mass.gov/locations/hampshire-county-superior-court +Hampshire,West Virginia,27,54,54027,county,Hampshire County,http://hampshirewv.com/ +Hampton,Virginia,650,51,51650,city,Hampton city,https://hampton.gov/ +Hampton,South Carolina,49,45,45049,county,Hampton County,https://www.hamptoncountysc.org/ +Hancock,Indiana,59,18,18059,county,Hancock County,https://www.hancockin.gov/ +Hancock,Tennessee,67,47,47067,county,Hancock County,http://www.hancockcountytn.com/ +Hancock,Maine,9,23,23009,county,Hancock County,https://hancockcountymaine.gov/ +Hancock,Ohio,63,39,39063,county,Hancock County,https://www.co.hancock.oh.us/ +Hancock,Mississippi,45,28,28045,county,Hancock County,https://hancockcounty.ms.gov/ +Hancock,West Virginia,29,54,54029,county,Hancock County,https://hancockcountywv.org/ +Hancock,Kentucky,91,21,21091,county,Hancock County,https://hancockky.us/ +Hancock,Georgia,141,13,13141,county,Hancock County,https://hancockcountyga.gov/ +Hancock,Iowa,81,19,19081,county,Hancock County,https://hancockcountyia.gov/ +Hancock,Illinois,67,17,17067,county,Hancock County,https://www.hancockcounty-il.gov/ +Hand,South Dakota,59,46,46059,county,Hand County,https://hand.sdcounties.org/ +Hanover,Virginia,85,51,51085,county,Hanover County,https://www.hanovercounty.gov/ +Hansford,Texas,195,48,48195,county,Hansford County,https://www.co.hansford.tx.us/ +Hanson,South Dakota,61,46,46061,county,Hanson County,https://www.hansoncounty.net/ +Haralson,Georgia,143,13,13143,county,Haralson County,https://www.haralsoncountyga.gov/ +Hardee,Florida,49,12,12049,county,Hardee County,https://www.hardeecountyfl.gov/ +Hardeman,Texas,197,48,48197,county,Hardeman County,https://www.co.hardeman.tx.us/ +Hardeman,Tennessee,69,47,47069,county,Hardeman County,https://hardemancounty.org/ +Hardin,Texas,199,48,48199,county,Hardin County,https://www.co.hardin.tx.us/ +Hardin,Ohio,65,39,39065,county,Hardin County,http://www.co.hardin.oh.us/ +Hardin,Iowa,83,19,19083,county,Hardin County,https://www.hardincountyia.gov/ +Hardin,Tennessee,71,47,47071,county,Hardin County,https://www.hardincogov.com/ +Hardin,Illinois,69,17,17069,county,Hardin County,https://www.ilsos.gov/departments/archives/IRAD/hardin.html +Hardin,Kentucky,93,21,21093,county,Hardin County,https://hcky.org/ +Harding,New Mexico,21,35,35021,county,Harding County,https://www.hardingcounty.org/ +Harding,South Dakota,63,46,46063,county,Harding County,https://ujs.sd.gov/Fourth_Circuit/Links/Counties.aspx?bWBs3hPIpsznJKeRKqDA3hl9C9BTQgW2Hl7VuETrhvk= +Hardy,West Virginia,31,54,54031,county,Hardy County,http://hardycounty.com/ +Harford,Maryland,25,24,24025,county,Harford County,https://www.harfordcountymd.gov/ +Harlan,Kentucky,95,21,21095,county,Harlan County,https://web.sos.ky.gov/land/Cities.aspx/?ctr=175 +Harlan,Nebraska,83,31,31083,county,Harlan County,https://harlancounty.ne.gov/ +Harmon,Oklahoma,57,40,40057,county,Harmon County,https://oklahoma.gov/okdhs/library/resources/harmonrd211.html +Harnett,North Carolina,85,37,37085,county,Harnett County,https://harnett.org/ +Harney,Oregon,25,41,41025,county,Harney County,https://www.harneycountyoregon.com/local-government +Harper,Kansas,77,20,20077,county,Harper County,https://www.harpercountyks.gov/ +Harper,Oklahoma,59,40,40059,county,Harper County,https://oklahoma.gov/okdhs/library/resources/harperrd211.html +Harris,Texas,201,48,48201,county,Harris County,https://www.harriscountytx.gov/ +Harris,Georgia,145,13,13145,county,Harris County,https://www.harriscountyga.gov/ +Harrison,Kentucky,97,21,21097,county,Harrison County,http://www.harrisoncountyfiscalcourt.com/ +Harrison,Mississippi,47,28,28047,county,Harrison County,https://harrisoncountyms.gov/ +Harrison,Texas,203,48,48203,county,Harrison County,https://harrisoncountytexas.org/ +Harrison,Ohio,67,39,39067,county,Harrison County,https://www.harrisoncountyohio.org/ +Harrison,Missouri,81,29,29081,county,Harrison County,https://www.mocounties.com/harrison-county +Harrison,Iowa,85,19,19085,county,Harrison County,https://harrisoncounty.iowa.gov/ +Harrison,Indiana,61,18,18061,county,Harrison County,https://www.harrisoncounty.in.gov/ +Harrison,West Virginia,33,54,54033,county,Harrison County,https://www.harrisoncountywv.com/ +Harrisonburg,Virginia,660,51,51660,city,Harrisonburg city,https://www.harrisonburgva.gov/ +Hart,Kentucky,99,21,21099,county,Hart County,https://hartcounty.ky.gov/ +Hart,Georgia,147,13,13147,county,Hart County,http://www.hartcountyga.gov/ +Hartley,Texas,205,48,48205,county,Hartley County,https://www.co.hartley.tx.us/ +Harvey,Kansas,79,20,20079,county,Harvey County,https://www.harveycounty.com/ +Haskell,Oklahoma,61,40,40061,county,Haskell County,https://oklahoma.gov/okdhs/library/resources/haskellrd211.html +Haskell,Kansas,81,20,20081,county,Haskell County,https://www.kansascounties.org/resources/county-websites +Haskell,Texas,207,48,48207,county,Haskell County,https://www.haskellcountytx.org/ +Hawkins,Tennessee,73,47,47073,county,Hawkins County,https://www.hawkinscountytn.gov/ +Hayes,Nebraska,85,31,31085,county,Hayes County,https://hayescounty.ne.gov/ +Hays,Texas,209,48,48209,county,Hays County,https://hayscountytx.com/ +Haywood,Tennessee,75,47,47075,county,Haywood County,https://haywoodtn.gov/ +Haywood,North Carolina,87,37,37087,county,Haywood County,https://www.haywoodcountync.gov/ +Heard,Georgia,149,13,13149,county,Heard County,https://www.heardcountyga.com/ +Hemphill,Texas,211,48,48211,county,Hemphill County,https://www.co.hemphill.tx.us/ +Hempstead,Arkansas,57,5,5057,county,Hempstead County,https://hempsteadcountyar.com/ +Henderson,Illinois,71,17,17071,county,Henderson County,https://www.ilsos.gov/departments/archives/IRAD/henderson.html +Henderson,North Carolina,89,37,37089,county,Henderson County,https://www.hendersoncountync.gov/home +Henderson,Tennessee,77,47,47077,county,Henderson County,https://www.hendersoncountytn.gov/ +Henderson,Kentucky,101,21,21101,county,Henderson County,https://hendersonky.us/ +Henderson,Texas,213,48,48213,county,Henderson County,https://www.henderson-county.com/ +Hendricks,Indiana,63,18,18063,county,Hendricks County,https://www.co.hendricks.in.us/ +Hendry,Florida,51,12,12051,county,Hendry County,https://www.hendryfla.net/ +Hennepin,Minnesota,53,27,27053,county,Hennepin County,https://www.hennepin.us/your-government/overview/overview-of-hennepin-county +Henrico,Virginia,87,51,51087,county,Henrico County,https://henrico.us/ +Henry,Iowa,87,19,19087,county,Henry County,https://henrycounty.iowa.gov/ +Henry,Indiana,65,18,18065,county,Henry County,http://www.henryco.net/ +Henry,Missouri,83,29,29083,county,Henry County,https://www.henrycomo.com/ +Henry,Ohio,69,39,39069,county,Henry County,https://www.henrycountyohio.gov/ +Henry,Virginia,89,51,51089,county,Henry County,https://www.henrycountyva.gov/ +Henry,Georgia,151,13,13151,county,Henry County,https://www.henrycountyga.gov/ +Henry,Tennessee,79,47,47079,county,Henry County,https://henrycountytn.org/ +Henry,Alabama,67,1,1067,county,Henry County,https://www.henrycountyal.com/ +Henry,Kentucky,103,21,21103,county,Henry County,https://henrycounty.ky.gov/Pages/index.aspx +Henry,Illinois,73,17,17073,county,Henry County,https://www.henrycty.com/ +Herkimer,New York,43,36,36043,county,Herkimer County,https://www.herkimercounty.org/ +Hernando,Florida,53,12,12053,county,Hernando County,https://www.hernandocounty.us/ +Hertford,North Carolina,91,37,37091,county,Hertford County,https://www.hertfordcountync.gov/ +Hettinger,North Dakota,41,38,38041,county,Hettinger County,https://www.hettingercountynd.com/ +Hickman,Kentucky,105,21,21105,county,Hickman County,https://hickman.countyclerk.us/ +Hickman,Tennessee,81,47,47081,county,Hickman County,https://hickmanco.com/ +Hickory,Missouri,85,29,29085,county,Hickory County,https://hickorycomo.net/ +Hidalgo,New Mexico,23,35,35023,county,Hidalgo County,https://hidalgocounty.org/ +Hidalgo,Texas,215,48,48215,county,Hidalgo County,https://www.hidalgocounty.us/ +Highland,Virginia,91,51,51091,county,Highland County,https://www.highlandcova.org/ +Highland,Ohio,71,39,39071,county,Highland County,https://co.highland.oh.us/ +Highlands,Florida,55,12,12055,county,Highlands County,https://www.highlandsfl.gov/ +Hill,Texas,217,48,48217,county,Hill County,https://www.co.hill.tx.us/ +Hill,Montana,41,30,30041,county,Hill County,https://hillcounty.us/ +Hillsborough,Florida,57,12,12057,county,Hillsborough County,https://www.hillsboroughcounty.org/ +Hillsborough,New Hampshire,11,33,33011,county,Hillsborough County,http://hcnh.org/ +Hillsdale,Michigan,59,26,26059,county,Hillsdale County,https://www.co.hillsdale.mi.us/ +Hinds,Mississippi,49,28,28049,county,Hinds County,https://www.hindscountyms.com/ +Hinsdale,Colorado,53,8,8053,county,Hinsdale County,https://hinsdalecounty.colorado.gov/ +Hitchcock,Nebraska,87,31,31087,county,Hitchcock County,https://hitchcockcounty.ne.gov/ +Hocking,Ohio,73,39,39073,county,Hocking County,https://hocking.oh.gov/ +Hockley,Texas,219,48,48219,county,Hockley County,https://www.co.hockley.tx.us/ +Hodgeman,Kansas,83,20,20083,county,Hodgeman County,https://hodgemancountyks.com/ +Hoke,North Carolina,93,37,37093,county,Hoke County,https://www.hokecounty.net/ +Holmes,Florida,59,12,12059,county,Holmes County,https://holmescountyfla.com/ +Holmes,Ohio,75,39,39075,county,Holmes County,https://www.co.holmes.oh.us/ +Holmes,Mississippi,51,28,28051,county,Holmes County,https://holmescountyms.org/ +Holt,Nebraska,89,31,31089,county,Holt County,https://holtcounty.nebraska.gov/ +Holt,Missouri,87,29,29087,county,Holt County,http://holtcounty.org/ +Hood,Texas,221,48,48221,county,Hood County,https://www.co.hood.tx.us/ +Hood River,Oregon,27,41,41027,county,Hood River County,https://www.hoodrivercounty.gov/ +Hooker,Nebraska,91,31,31091,county,Hooker County,https://co.hooker.ne.us/ +Hopewell,Virginia,670,51,51670,city,Hopewell city,https://hopewellva.gov/ +Hopkins,Kentucky,107,21,21107,county,Hopkins County,https://hopkinscounty.ky.gov/ +Hopkins,Texas,223,48,48223,county,Hopkins County,https://www.hopkinscountytx.org/ +Horry,South Carolina,51,45,45051,county,Horry County,https://www.horrycountysc.gov/ +Hot Spring,Arkansas,59,5,5059,county,Hot Spring County,https://hotspringcounty.org/ +Hot Springs,Wyoming,17,56,56017,county,Hot Springs County,https://hscounty.com/ +Houghton,Michigan,61,26,26061,county,Houghton County,https://www.houghtoncounty.net/ +Houston,Tennessee,83,47,47083,county,Houston County,https://www.explorehoustoncountytn.com/ +Houston,Georgia,153,13,13153,county,Houston County,https://www.houstoncountyga.org/ +Houston,Minnesota,55,27,27055,county,Houston County,https://www.co.houston.mn.us/ +Houston,Alabama,69,1,1069,county,Houston County,https://houstoncountyal.gov/ +Houston,Texas,225,48,48225,county,Houston County,https://www.co.houston.tx.us/ +Howard,Nebraska,93,31,31093,county,Howard County,https://howardcounty.ne.gov/ +Howard,Texas,227,48,48227,county,Howard County,https://www.co.howard.tx.us/ +Howard,Missouri,89,29,29089,county,Howard County,https://www.mocounties.com/howard-county +Howard,Indiana,67,18,18067,county,Howard County,http://www.howardcountyin.gov/ +Howard,Iowa,89,19,19089,county,Howard County,https://howardcounty.iowa.gov/ +Howard,Maryland,27,24,24027,county,Howard County,https://www.howardcountymd.gov/ +Howard,Arkansas,61,5,5061,county,Howard County,https://local.arkansas.gov/local.php?agency=Howard%20County +Howell,Missouri,91,29,29091,county,Howell County,https://howellcounty.net/ +Hubbard,Minnesota,57,27,27057,county,Hubbard County,https://www.co.hubbard.mn.us/ +Hudson,New Jersey,17,34,34017,county,Hudson County,https://www.hcnj.us/ +Hudspeth,Texas,229,48,48229,county,Hudspeth County,https://www.co.hudspeth.tx.us/ +Huerfano,Colorado,55,8,8055,county,Huerfano County,https://huerfano.us/ +Hughes,South Dakota,65,46,46065,county,Hughes County,https://www.hughescounty.org/ +Hughes,Oklahoma,63,40,40063,county,Hughes County,https://oklahoma.gov/okdhs/library/resources/hughesrd211.html +Humboldt,Iowa,91,19,19091,county,Humboldt County,https://www.humboldtcounty.iowa.gov/ +Humboldt,California,23,6,6023,county,Humboldt County,http://humboldtgov.org/ +Humboldt,Nevada,13,32,32013,county,Humboldt County,https://www.humboldtcountynv.gov/ +Humphreys,Tennessee,85,47,47085,county,Humphreys County,https://www.humphreystn.com/ +Humphreys,Mississippi,53,28,28053,county,Humphreys County,https://humphreyscounty.ms/ +Hunt,Texas,231,48,48231,county,Hunt County,https://www.huntcounty.net/ +Hunterdon,New Jersey,19,34,34019,county,Hunterdon County,https://www.co.hunterdon.nj.us/ +Huntingdon,Pennsylvania,61,42,42061,county,Huntingdon County,https://www.huntingdoncounty.net/ +Huntington,Indiana,69,18,18069,county,Huntington County,https://www.huntington.in.us/county/ +Huron,Michigan,63,26,26063,county,Huron County,https://www.co.huron.mi.us/ +Huron,Ohio,77,39,39077,county,Huron County,http://www.huroncounty-oh.gov/ +Hutchinson,South Dakota,67,46,46067,county,Hutchinson County,https://hutchinsoncountysd.org/ +Hutchinson,Texas,233,48,48233,county,Hutchinson County,https://www.co.hutchinson.tx.us/ +Hyde,South Dakota,69,46,46069,county,Hyde County,http://hydetreas.users.venturecomm.net/index.htm +Hyde,North Carolina,95,37,37095,county,Hyde County,https://www.hydecountync.gov/ +Iberia,Louisiana,45,22,22045,parish,Iberia Parish,http://iberiaparishgovernment.com/ +Iberville,Louisiana,47,22,22047,parish,Iberville Parish,https://ibervilleparish.com/ +Ida,Iowa,93,19,19093,county,Ida County,https://idacounty.iowa.gov/ +Idaho,Idaho,49,16,16049,county,Idaho County,https://idaho.gov/counties/idaho/ +Imperial,California,25,6,6025,county,Imperial County,https://imperialcounty.org/ +Independence,Arkansas,63,5,5063,county,Independence County,https://local.arkansas.gov/local.php?agency=Independence%20County +Indian River,Florida,61,12,12061,county,Indian River County,https://indianriver.gov/ +Indiana,Pennsylvania,63,42,42063,county,Indiana County,https://www.indianacountypa.gov/ +Ingham,Michigan,65,26,26065,county,Ingham County,https://www.ingham.org/ +Inyo,California,27,6,6027,county,Inyo County,https://www.inyocounty.us/node/1 +Ionia,Michigan,67,26,26067,county,Ionia County,https://ioniacounty.org/ +Iosco,Michigan,69,26,26069,county,Iosco County,https://iosco.net/ +Iowa,Wisconsin,49,55,55049,county,Iowa County,https://www.iowacounty.org/ +Iowa,Iowa,95,19,19095,county,Iowa County,https://iowacounty.iowa.gov/ +Iredell,North Carolina,97,37,37097,county,Iredell County,https://www.iredellcountync.gov/ +Irion,Texas,235,48,48235,county,Irion County,https://www.co.irion.tx.us/ +Iron,Michigan,71,26,26071,county,Iron County,https://ironmi.com/ +Iron,Missouri,93,29,29093,county,Iron County,https://ironcountymo.gov/ +Iron,Wisconsin,51,55,55051,county,Iron County,http://www.co.iron.wi.gov/ +Iron,Utah,21,49,49021,county,Iron County,https://www.ironcounty.net/ +Iroquois,Illinois,75,17,17075,county,Iroquois County,https://iroquoiscountyil.gov/ +Irwin,Georgia,155,13,13155,county,Irwin County,https://irwincounty-ga.gov/ +Isabella,Michigan,73,26,26073,county,Isabella County,https://www.isabellacounty.org/ +Isanti,Minnesota,59,27,27059,county,Isanti County,https://www.co.isanti.mn.us/ +Island,Washington,29,53,53029,county,Island County,https://www.islandcountywa.gov/ +Isle of Wight,Virginia,93,51,51093,county,Isle of Wight County,https://www.co.isle-of-wight.va.us/ +Issaquena,Mississippi,55,28,28055,county,Issaquena County,https://www.mssupervisors.org/ms-counties/issaquena +Itasca,Minnesota,61,27,27061,county,Itasca County,https://www.co.itasca.mn.us/ +Itawamba,Mississippi,57,28,28057,county,Itawamba County,https://www.mssupervisors.org/ms-counties/itawamba +Izard,Arkansas,65,5,5065,county,Izard County,https://www.izardcountyar.org/ +Jack,Texas,237,48,48237,county,Jack County,https://www.jackcounty.org/ +Jackson,Ohio,79,39,39079,county,Jackson County,https://www.jacksoncountyohio.us/ +Jackson,South Dakota,71,46,46071,county,Jackson County,https://ujs.sd.gov/Sixth_Circuit/Links/Counties.aspx +Jackson,Tennessee,87,47,47087,county,Jackson County,https://www.jacksoncotn.com/ +Jackson,Iowa,97,19,19097,county,Jackson County,https://jacksoncounty.iowa.gov/ +Jackson,Missouri,95,29,29095,county,Jackson County,https://www.jacksongov.org/Home +Jackson,Texas,239,48,48239,county,Jackson County,https://www.co.jackson.tx.us/ +Jackson,Wisconsin,53,55,55053,county,Jackson County,https://www.co.jackson.wi.us/ +Jackson,Colorado,57,8,8057,county,Jackson County,https://jacksoncountyco.gov/ +Jackson,Indiana,71,18,18071,county,Jackson County,https://www.jacksoncounty.in.gov/ +Jackson,Florida,63,12,12063,county,Jackson County,https://jacksoncountyfl.gov/ +Jackson,North Carolina,99,37,37099,county,Jackson County,https://www.jacksonnc.org/ +Jackson,Georgia,157,13,13157,county,Jackson County,https://www.jacksoncountygov.com/ +Jackson,Alabama,71,1,1071,county,Jackson County,https://www.jacksoncountyal.gov/ +Jackson,West Virginia,35,54,54035,county,Jackson County,https://jacksoncounty.wv.gov/ +Jackson,Arkansas,67,5,5067,county,Jackson County,https://jacksoncountyar.com/ +Jackson,Michigan,75,26,26075,county,Jackson County,https://www.mijackson.org/ +Jackson,Oregon,29,41,41029,county,Jackson County,https://www.jacksoncountyor.gov/ +Jackson,Mississippi,59,28,28059,county,Jackson County,https://www.co.jackson.ms.us/ +Jackson,Kentucky,109,21,21109,county,Jackson County,https://jacksoncounty.ky.gov/ +Jackson,Kansas,85,20,20085,county,Jackson County,https://www.jacksoncountyks.com/ +Jackson,Illinois,77,17,17077,county,Jackson County,https://www.jacksoncounty-il.gov/ +Jackson,Minnesota,63,27,27063,county,Jackson County,https://www.co.jackson.mn.us/ +Jackson,Oklahoma,65,40,40065,county,Jackson County,http://www.jacksoncountyok.com/ +Jackson,Louisiana,49,22,22049,parish,Jackson Parish,https://www.louisiana.gov/local-louisiana/jackson-parish +James City,Virginia,95,51,51095,county,James City County,https://www.jamescitycountyva.gov/ +Jasper,Georgia,159,13,13159,county,Jasper County,https://jaspercountyga.org/ +Jasper,Missouri,97,29,29097,county,Jasper County,https://www.jaspercountymo.gov/ +Jasper,South Carolina,53,45,45053,county,Jasper County,https://www.jaspercountysc.gov/ +Jasper,Indiana,73,18,18073,county,Jasper County,https://www.jaspercountyin.gov/ +Jasper,Iowa,99,19,19099,county,Jasper County,https://co.jasper.ia.us/ +Jasper,Texas,241,48,48241,county,Jasper County,https://www.co.jasper.tx.us/ +Jasper,Illinois,79,17,17079,county,Jasper County,https://www.ilsos.gov/departments/archives/IRAD/jasper.html +Jasper,Mississippi,61,28,28061,county,Jasper County,https://www.co.jasper.ms.us/ +Jay,Indiana,75,18,18075,county,Jay County,https://jaycounty.net/ +Jeff Davis,Texas,243,48,48243,county,Jeff Davis County,https://www.jeffdaviscounty.texas.gov/ +Jeff Davis,Georgia,161,13,13161,county,Jeff Davis County,https://jeffdaviscountyga.gov/ +Jefferson,Oklahoma,67,40,40067,county,Jefferson County,https://oklahoma.gov/okdhs/library/resources/jeffersonrd211.html +Jefferson,Montana,43,30,30043,county,Jefferson County,https://jeffersoncounty-mt.gov/ +Jefferson,Illinois,81,17,17081,county,Jefferson County,http://www.jeffersoncountyillinois.com/ +Jefferson,Oregon,31,41,41031,county,Jefferson County,https://www.jeffco.net/home +Jefferson,Arkansas,69,5,5069,county,Jefferson County,https://www.jeffersoncountyar.gov/ +Jefferson,Kansas,87,20,20087,county,Jefferson County,https://www.jfcountyks.com/ +Jefferson,Mississippi,63,28,28063,county,Jefferson County,http://www.jeffersoncountyms.com/ +Jefferson,West Virginia,37,54,54037,county,Jefferson County,https://www.jeffersoncountywv.org/ +Jefferson,Pennsylvania,65,42,42065,county,Jefferson County,https://www.jeffersoncountypa.com/ +Jefferson,Texas,245,48,48245,county,Jefferson County,https://co.jefferson.tx.us/ +Jefferson,Wisconsin,55,55,55055,county,Jefferson County,https://www.jeffersoncountywi.gov/ +Jefferson,Georgia,163,13,13163,county,Jefferson County,https://www.jeffersoncountyga.gov/ +Jefferson,Idaho,51,16,16051,county,Jefferson County,https://www.co.jefferson.id.us/ +Jefferson,New York,45,36,36045,county,Jefferson County,https://co.jefferson.ny.us/ +Jefferson,Ohio,81,39,39081,county,Jefferson County,https://jeffersoncountyoh.com/ +Jefferson,Kentucky,111,21,21111,county,Jefferson County,https://louisvilleky.gov/ +Jefferson,Iowa,101,19,19101,county,Jefferson County,https://jeffersoncounty.iowa.gov/ +Jefferson,Florida,65,12,12065,county,Jefferson County,http://www.jeffersoncountyfl.gov/ +Jefferson,Colorado,59,8,8059,county,Jefferson County,https://www.jeffco.us/ +Jefferson,Missouri,99,29,29099,county,Jefferson County,https://www.jeffcomo.org/ +Jefferson,Louisiana,51,22,22051,parish,Jefferson Parish,https://www.jeffparish.net/ +Jefferson,Washington,31,53,53031,county,Jefferson County,https://www.co.jefferson.wa.us/ +Jefferson,Nebraska,95,31,31095,county,Jefferson County,https://jeffersoncounty.nebraska.gov/ +Jefferson,Alabama,73,1,1073,county,Jefferson County,https://www.jccal.org/ +Jefferson,Tennessee,89,47,47089,county,Jefferson County,https://jeffersoncountytn.gov/ +Jefferson,Indiana,77,18,18077,county,Jefferson County,https://jeffersoncounty.in.gov/ +Jefferson Davis,Mississippi,65,28,28065,county,Jefferson Davis County,https://www.jeffersondaviscountyms.com/ +Jefferson Davis,Louisiana,53,22,22053,parish,Jefferson Davis Parish,https://jeffdavis.org/ +Jenkins,Georgia,165,13,13165,county,Jenkins County,http://www.jenkinscountyga.com/ +Jennings,Indiana,79,18,18079,county,Jennings County,https://jenningscounty-in.gov/ +Jerauld,South Dakota,73,46,46073,county,Jerauld County,https://jerauldcountysd.com/ +Jerome,Idaho,53,16,16053,county,Jerome County,https://www.jeromecountyid.us/ +Jersey,Illinois,83,17,17083,county,Jersey County,https://jerseycountyillinois.us/ +Jessamine,Kentucky,113,21,21113,county,Jessamine County,https://jessamineco.com/ +Jewell,Kansas,89,20,20089,county,Jewell County,https://jewellcountykansas.net/ +Jim Hogg,Texas,247,48,48247,county,Jim Hogg County,https://www.co.jim-hogg.tx.us/ +Jim Wells,Texas,249,48,48249,county,Jim Wells County,https://www.co.jim-wells.tx.us/ +Jo Daviess,Illinois,85,17,17085,county,Jo Daviess County,https://www.jodaviesscountyil.gov/ +Johnson,Illinois,87,17,17087,county,Johnson County,https://www.ilsos.gov/departments/archives/IRAD/johnson.html +Johnson,Kansas,91,20,20091,county,Johnson County,https://www.jocogov.org/ +Johnson,Kentucky,115,21,21115,county,Johnson County,https://www.johnsoncoky.com/ +Johnson,Georgia,167,13,13167,county,Johnson County,https://www.johnsonco.org/home +Johnson,Iowa,103,19,19103,county,Johnson County,https://www.johnsoncountyiowa.gov/ +Johnson,Texas,251,48,48251,county,Johnson County,https://www.johnsoncountytx.org/ +Johnson,Tennessee,91,47,47091,county,Johnson County,https://www.johnsoncountytn.gov/ +Johnson,Indiana,81,18,18081,county,Johnson County,https://co.johnson.in.us/ +Johnson,Missouri,101,29,29101,county,Johnson County,https://www.jococourthouse.com/ +Johnson,Arkansas,71,5,5071,county,Johnson County,https://johnsoncounty.arkansas.gov/ +Johnson,Nebraska,97,31,31097,county,Johnson County,https://johnsoncounty.ne.gov/ +Johnson,Wyoming,19,56,56019,county,Johnson County,http://www.johnsoncountywyoming.org/ +Johnston,North Carolina,101,37,37101,county,Johnston County,https://www.johnstonnc.com/ +Johnston,Oklahoma,69,40,40069,county,Johnston County,https://oklahoma.gov/okdhs/library/resources/johnstonrd211.html +Jones,Mississippi,67,28,28067,county,Jones County,https://jonescountyms.com/ +Jones,Georgia,169,13,13169,county,Jones County,https://www.jonescountyga.org/ +Jones,South Dakota,75,46,46075,county,Jones County,https://murdosd.com/government-offices +Jones,North Carolina,103,37,37103,county,Jones County,https://jonescountync.gov/ +Jones,Texas,253,48,48253,county,Jones County,https://www.co.jones.tx.us/ +Jones,Iowa,105,19,19105,county,Jones County,https://www.jonescountyiowa.gov/ +Josephine,Oregon,33,41,41033,county,Josephine County,https://www.josephinecounty.gov/ +Juab,Utah,23,49,49023,county,Juab County,https://juabcounty.gov/ +Judith Basin,Montana,45,30,30045,county,Judith Basin County,http://www.jbcounty.org/departments +Juneau,Wisconsin,57,55,55057,county,Juneau County,https://www.co.juneau.wi.gov/ +Juniata,Pennsylvania,67,42,42067,county,Juniata County,https://www.juniataco.org/ +Kalamazoo,Michigan,77,26,26077,county,Kalamazoo County,https://www.kalcounty.com/ +Kalkaska,Michigan,79,26,26079,county,Kalkaska County,https://www.kalkaskacounty.net/ +Kanabec,Minnesota,65,27,27065,county,Kanabec County,https://www.kanabeccounty.org/ +Kanawha,West Virginia,39,54,54039,county,Kanawha County,https://kanawha.us/ +Kandiyohi,Minnesota,67,27,27067,county,Kandiyohi County,https://www.kcmn.us/ +Kane,Illinois,89,17,17089,county,Kane County,https://www.countyofkane.org/ +Kane,Utah,25,49,49025,county,Kane County,https://kane.utah.gov/ +Kankakee,Illinois,91,17,17091,county,Kankakee County,https://www.k3county.net/ +Karnes,Texas,255,48,48255,county,Karnes County,https://www.co.karnes.tx.us/ +Kaufman,Texas,257,48,48257,county,Kaufman County,https://www.kaufmancounty.net/ +Kay,Oklahoma,71,40,40071,county,Kay County,https://www.courthouse.kay.ok.us/ +Kearney,Nebraska,99,31,31099,county,Kearney County,https://kearneycounty.ne.gov/ +Kearny,Kansas,93,20,20093,county,Kearny County,https://www.kearnycountykansas.com/ +Keith,Nebraska,101,31,31101,county,Keith County,https://www.keithcountyne.gov/ +Kemper,Mississippi,69,28,28069,county,Kemper County,https://www.kempercounty.ms/ +Kendall,Texas,259,48,48259,county,Kendall County,https://www.co.kendall.tx.us/ +Kendall,Illinois,93,17,17093,county,Kendall County,https://www.kendallcountyil.gov/ +Kenedy,Texas,261,48,48261,county,Kenedy County,https://www.co.kenedy.tx.us/ +Kennebec,Maine,11,23,23011,county,Kennebec County,https://kennebec.gov/ +Kenosha,Wisconsin,59,55,55059,county,Kenosha County,https://www.kenoshacounty.org/ +Kent,Texas,263,48,48263,county,Kent County,https://kentcountytexas.us/home +Kent,Michigan,81,26,26081,county,Kent County,https://www.accesskent.com/ +Kent,Rhode Island,3,44,44003,county,Kent County,https://www.ri.gov/ +Kent,Delaware,1,10,10001,county,Kent County,https://www.kentcountyde.gov/Your-County-Government +Kent,Maryland,29,24,24029,county,Kent County,https://www.kentcounty.com/ +Kenton,Kentucky,117,21,21117,county,Kenton County,https://www.kentoncounty.org/ +Keokuk,Iowa,107,19,19107,county,Keokuk County,https://keokukcounty.iowa.gov/ +Kern,California,29,6,6029,county,Kern County,https://www.kerncounty.com/ +Kerr,Texas,265,48,48265,county,Kerr County,https://kerrcountytx.gov/ +Kershaw,South Carolina,55,45,45055,county,Kershaw County,https://www.kershaw.sc.gov/ +Kewaunee,Wisconsin,61,55,55061,county,Kewaunee County,https://www.kewauneeco.org/ +Keweenaw,Michigan,83,26,26083,county,Keweenaw County,https://www.keweenawcountyonline.org/ +Keya Paha,Nebraska,103,31,31103,county,Keya Paha County,https://co.keya-paha.ne.us/ +Kidder,North Dakota,43,38,38043,county,Kidder County,https://www.ndaco.org/cod/browse-by-county/ +Kimball,Nebraska,105,31,31105,county,Kimball County,https://www.kimballcountyne.gov/ +Kimble,Texas,267,48,48267,county,Kimble County,https://www.co.kimble.tx.us/ +King,Washington,33,53,53033,county,King County,https://kingcounty.gov/en/ +King,Texas,269,48,48269,county,King County,https://www.co.king.tx.us/ +King George,Virginia,99,51,51099,county,King George County,https://www.kinggeorgecountyva.gov/ +King William,Virginia,101,51,51101,county,King William County,https://www.kwc.gov/ +King and Queen,Virginia,97,51,51097,county,King and Queen County,http://www.kingandqueenco.net/ +Kingfisher,Oklahoma,73,40,40073,county,Kingfisher County,https://kingfisher.okcounties.org/ +Kingman,Kansas,95,20,20095,county,Kingman County,https://www.kingmancoks.org/ +Kings,New York,47,36,36047,county,Kings County,"https://www.britannica.com/place/Kings#:~:text=Kings%2C%20county%20in%20southeastern%20New,King%20Charles%20II%20of%20England." +Kings,California,31,6,6031,county,Kings County,https://www.countyofkings.com/ +Kingsbury,South Dakota,77,46,46077,county,Kingsbury County,https://kingsburycountysd.org/ +Kinney,Texas,271,48,48271,county,Kinney County,https://www.co.kinney.tx.us/ +Kiowa,Kansas,97,20,20097,county,Kiowa County,https://kiowacountyks.org/ +Kiowa,Colorado,61,8,8061,county,Kiowa County,https://www.kiowacounty-colorado.com/ +Kiowa,Oklahoma,75,40,40075,county,Kiowa County,https://kiowacountyok.us/ +Kit Carson,Colorado,63,8,8063,county,Kit Carson County,https://kitcarsoncounty.colorado.gov/ +Kitsap,Washington,35,53,53035,county,Kitsap County,https://www.kitsapgov.com/ +Kittitas,Washington,37,53,53037,county,Kittitas County,https://www.co.kittitas.wa.us/ +Kittson,Minnesota,69,27,27069,county,Kittson County,https://www.co.kittson.mn.us/ +Klamath,Oregon,35,41,41035,county,Klamath County,https://www.klamathcounty.org/ +Kleberg,Texas,273,48,48273,county,Kleberg County,https://www.co.kleberg.tx.us/ +Klickitat,Washington,39,53,53039,county,Klickitat County,https://www.klickitatcounty.org/ +Knott,Kentucky,119,21,21119,county,Knott County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Knott+County +Knox,Ohio,83,39,39083,county,Knox County,https://co.knox.oh.us/ +Knox,Illinois,95,17,17095,county,Knox County,https://co.knox.il.us/ +Knox,Tennessee,93,47,47093,county,Knox County,https://www.knoxcounty.org/ +Knox,Maine,13,23,23013,county,Knox County,https://www.knoxcountymaine.gov/ +Knox,Indiana,83,18,18083,county,Knox County,https://knoxcounty.in.gov/ +Knox,Missouri,103,29,29103,county,Knox County,https://www.knoxcountymo.org/ +Knox,Texas,275,48,48275,county,Knox County,https://www.knoxcountytexas.org/ +Knox,Nebraska,107,31,31107,county,Knox County,https://co.knox.ne.us/ +Knox,Kentucky,121,21,21121,county,Knox County,https://knoxfiscalcourt.com/ +Koochiching,Minnesota,71,27,27071,county,Koochiching County,https://www.co.koochiching.mn.us/ +Kootenai,Idaho,55,16,16055,county,Kootenai County,https://www.kcgov.us/ +Kosciusko,Indiana,85,18,18085,county,Kosciusko County,https://www.kcgov.com/ +Kossuth,Iowa,109,19,19109,county,Kossuth County,https://www.kossuthcounty.iowa.gov/ +La Crosse,Wisconsin,63,55,55063,county,La Crosse County,https://www.lacrossecounty.org/ +La Paz,Arizona,12,4,4012,county,La Paz County,https://www.co.la-paz.az.us/ +La Plata,Colorado,67,8,8067,county,La Plata County,https://www.co.laplata.co.us/ +La Salle,Texas,283,48,48283,county,La Salle County,https://www.co.la-salle.tx.us/ +LaGrange,Indiana,87,18,18087,county,LaGrange County,https://www.lagrangecounty.org/ +LaMoure,North Dakota,45,38,38045,county,LaMoure County,https://lamourecountynd.com/ +LaPorte,Indiana,91,18,18091,county,LaPorte County,https://laporteco.in.gov/ +LaSalle,Louisiana,59,22,22059,parish,LaSalle Parish,https://www.louisiana.gov/local-louisiana/lasalle-parish +LaSalle,Illinois,99,17,17099,county,LaSalle County,https://www.lasallecountyil.gov/ +Labette,Kansas,99,20,20099,county,Labette County,https://labettecounty.com/ +Lac qui Parle,Minnesota,73,27,27073,county,Lac qui Parle County,https://www.lqpco.com/ +Lackawanna,Pennsylvania,69,42,42069,county,Lackawanna County,https://www.lackawannacounty.org/ +Laclede,Missouri,105,29,29105,county,Laclede County,https://lacledecountymissouri.org/ +Lafayette,Florida,67,12,12067,county,Lafayette County,https://lafayettecountyfl.org/ +Lafayette,Mississippi,71,28,28071,county,Lafayette County,https://lafayettems.com/ +Lafayette,Louisiana,55,22,22055,parish,Lafayette Parish,https://www.louisiana.gov/local-louisiana/lafayette-parish +Lafayette,Wisconsin,65,55,55065,county,Lafayette County,https://www.lafayettecountywi.org/home +Lafayette,Arkansas,73,5,5073,county,Lafayette County,https://www.lafayettecounty.arkansas.gov/ +Lafayette,Missouri,107,29,29107,county,Lafayette County,https://www.lafayettecountymo.com/ +Lafourche,Louisiana,57,22,22057,parish,Lafourche Parish,https://www.lafourchegov.org/ +Lake,Ohio,85,39,39085,county,Lake County,https://www.lakecountyohio.gov/ +Lake,Florida,69,12,12069,county,Lake County,https://www.lakecountyfl.gov/ +Lake,Tennessee,95,47,47095,county,Lake County,https://lakecountytn.com/ +Lake,Montana,47,30,30047,county,Lake County,https://www.lakemt.gov/ +Lake,Illinois,97,17,17097,county,Lake County,https://www.lakecountyil.gov/ +Lake,South Dakota,79,46,46079,county,Lake County,http://www.lake.sd.gov/ +Lake,Oregon,37,41,41037,county,Lake County,https://www.lakecountyor.org/ +Lake,Minnesota,75,27,27075,county,Lake County,https://www.co.lake.mn.us/ +Lake,Indiana,89,18,18089,county,Lake County,https://lakecounty.in.gov/ +Lake,California,33,6,6033,county,Lake County,https://www.lakecountyca.gov/ +Lake,Colorado,65,8,8065,county,Lake County,https://www.lakecountyco.com/ +Lake,Michigan,85,26,26085,county,Lake County,http://www.lakecounty-michigan.com/ +Lake of the Woods,Minnesota,77,27,27077,county,Lake of the Woods County,https://www.co.lake-of-the-woods.mn.us/ +Lamar,Mississippi,73,28,28073,county,Lamar County,https://lamarcountyms.gov/ms/ +Lamar,Alabama,75,1,1075,county,Lamar County,https://www.sos.alabama.gov/city-county-lookup/lamar +Lamar,Georgia,171,13,13171,county,Lamar County,https://www.lamarcountyga.com/ +Lamar,Texas,277,48,48277,county,Lamar County,https://www.co.lamar.tx.us/ +Lamb,Texas,279,48,48279,county,Lamb County,https://www.co.lamb.tx.us/ +Lamoille,Vermont,15,50,50015,county,Lamoille County,http://www.vermont.gov/ +Lampasas,Texas,281,48,48281,county,Lampasas County,https://www.co.lampasas.tx.us/ +Lancaster,Virginia,103,51,51103,county,Lancaster County,http://www.lancova.com/ +Lancaster,South Carolina,57,45,45057,county,Lancaster County,https://www.mylancastersc.org/ +Lancaster,Pennsylvania,71,42,42071,county,Lancaster County,https://co.lancaster.pa.us/ +Lancaster,Nebraska,109,31,31109,county,Lancaster County,https://www.lancaster.ne.gov/ +Lander,Nevada,15,32,32015,county,Lander County,https://www.landercountynv.org/ +Lane,Oregon,39,41,41039,county,Lane County,https://lanecounty.org/ +Lane,Kansas,101,20,20101,county,Lane County,http://www.lanecountyks.org/ +Langlade,Wisconsin,67,55,55067,county,Langlade County,https://www.co.langlade.wi.us/ +Lanier,Georgia,173,13,13173,county,Lanier County,http://laniercountyboc.com/wp/ +Lapeer,Michigan,87,26,26087,county,Lapeer County,https://www.lapeercountymi.gov/ +Laramie,Wyoming,21,56,56021,county,Laramie County,https://www.laramiecountywy.gov/ +Larimer,Colorado,69,8,8069,county,Larimer County,https://www.larimer.gov/ +Larue,Kentucky,123,21,21123,county,Larue County,https://laruecountyky.gov/ +Las Animas,Colorado,71,8,8071,county,Las Animas County,https://lasanimascounty.colorado.gov/ +Lassen,California,35,6,6035,county,Lassen County,https://www.lassencounty.org/ +Latah,Idaho,57,16,16057,county,Latah County,https://latahcountyid.gov/ +Latimer,Oklahoma,77,40,40077,county,Latimer County,https://latimer.okcounties.org/ +Lauderdale,Alabama,77,1,1077,county,Lauderdale County,https://lauderdalecountyal.gov/ +Lauderdale,Mississippi,75,28,28075,county,Lauderdale County,https://lauderdalecounty.org/ +Lauderdale,Tennessee,97,47,47097,county,Lauderdale County,https://lauderdalecountytn.org/our-county/government/ +Laurel,Kentucky,125,21,21125,county,Laurel County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Laurel+County +Laurens,Georgia,175,13,13175,county,Laurens County,https://www.laurenscoga.org/ +Laurens,South Carolina,59,45,45059,county,Laurens County,https://laurenscounty.us/ +Lavaca,Texas,285,48,48285,county,Lavaca County,https://www.co.lavaca.tx.us/ +Lawrence,Alabama,79,1,1079,county,Lawrence County,http://www.lawrencecountyal.org/ +Lawrence,Ohio,87,39,39087,county,Lawrence County,https://lawrencecounty.org/ +Lawrence,Arkansas,75,5,5075,county,Lawrence County,http://www.lawrencecountyarkansas.com/ +Lawrence,South Dakota,81,46,46081,county,Lawrence County,https://www.lawrence.sd.us/ +Lawrence,Mississippi,77,28,28077,county,Lawrence County,https://lawrencecountyms.com/ +Lawrence,Kentucky,127,21,21127,county,Lawrence County,https://lawrencecounty.ky.gov/ +Lawrence,Pennsylvania,73,42,42073,county,Lawrence County,https://lawrencecountypa.gov/ +Lawrence,Missouri,109,29,29109,county,Lawrence County,https://www.lawrencecountymo.org/ +Lawrence,Tennessee,99,47,47099,county,Lawrence County,https://www.lawrenceburgtn.gov/ +Lawrence,Indiana,93,18,18093,county,Lawrence County,https://lawrencecounty.in.gov/ +Lawrence,Illinois,101,17,17101,county,Lawrence County,https://lawrencecounty.illinois.gov/ +Le Flore,Oklahoma,79,40,40079,county,Le Flore County,https://oklahoma.gov/health/locations/county-health-departments/leflore-county-health-department.html +Le Sueur,Minnesota,79,27,27079,county,Le Sueur County,https://www.co.le-sueur.mn.us/ +Lea,New Mexico,25,35,35025,county,Lea County,https://www.leacounty.net/ +Leake,Mississippi,79,28,28079,county,Leake County,https://leakecountyms.org/ +Leavenworth,Kansas,103,20,20103,county,Leavenworth County,https://www.leavenworthcounty.gov/ +Lebanon,Pennsylvania,75,42,42075,county,Lebanon County,https://lebanoncountypa.gov/ +Lee,Georgia,177,13,13177,county,Lee County,http://www.lee.ga.us/ +Lee,North Carolina,105,37,37105,county,Lee County,https://leecountync.gov/ +Lee,Kentucky,129,21,21129,county,Lee County,https://leecounty.ky.gov/ +Lee,Iowa,111,19,19111,county,Lee County,https://www.leecounty.org/ +Lee,South Carolina,61,45,45061,county,Lee County,https://www.leecountysc.org/ +Lee,Florida,71,12,12071,county,Lee County,https://www.leegov.com/ +Lee,Texas,287,48,48287,county,Lee County,https://www.co.lee.tx.us/ +Lee,Illinois,103,17,17103,county,Lee County,https://www.leecountyil.com/ +Lee,Alabama,81,1,1081,county,Lee County,http://www.leeco.us/ +Lee,Arkansas,77,5,5077,county,Lee County,https://local.arkansas.gov/local.php?agency=Lee%20County +Lee,Mississippi,81,28,28081,county,Lee County,https://www.mssupervisors.org/ms-counties/lee +Lee,Virginia,105,51,51105,county,Lee County,http://www.leecova.org/ +Leelanau,Michigan,89,26,26089,county,Leelanau County,https://www.leelanau.gov/ +Leflore,Mississippi,83,28,28083,county,Leflore County,https://www.mssupervisors.org/ms-counties/leflore +Lehigh,Pennsylvania,77,42,42077,county,Lehigh County,https://www.lehighcounty.org/ +Lemhi,Idaho,59,16,16059,county,Lemhi County,https://www.lemhicountyidaho.org/ +Lenawee,Michigan,91,26,26091,county,Lenawee County,https://www.lenawee.mi.us/ +Lenoir,North Carolina,107,37,37107,county,Lenoir County,https://lenoircountync.gov/ +Leon,Florida,73,12,12073,county,Leon County,https://www.leoncountyfl.gov/ +Leon,Texas,289,48,48289,county,Leon County,https://www.co.leon.tx.us/ +Leslie,Kentucky,131,21,21131,county,Leslie County,https://lesliecounty.ky.gov/ +Letcher,Kentucky,133,21,21133,county,Letcher County,https://www.letchercounty.ky.gov/ +Levy,Florida,75,12,12075,county,Levy County,https://www.levycounty.org/ +Lewis,West Virginia,41,54,54041,county,Lewis County,https://www.lewiscountywv.org/ +Lewis,Kentucky,135,21,21135,county,Lewis County,http://lewiscountyky.gov/ +Lewis,Washington,41,53,53041,county,Lewis County,https://lewiscountywa.gov/ +Lewis,Tennessee,101,47,47101,county,Lewis County,https://www.lewiscountytn.com/ +Lewis,New York,49,36,36049,county,Lewis County,https://lewiscountyny.gov/ +Lewis,Idaho,61,16,16061,county,Lewis County,http://www.lewiscountyid.us/ +Lewis,Missouri,111,29,29111,county,Lewis County,https://lewiscountymo.org/ +Lewis and Clark,Montana,49,30,30049,county,Lewis and Clark County,https://www.lccountymt.gov/Home +Lexington,Virginia,678,51,51678,city,Lexington city,https://www.lexingtonva.gov/ +Lexington,South Carolina,63,45,45063,county,Lexington County,https://lex-co.sc.gov/ +Liberty,Florida,77,12,12077,county,Liberty County,https://libertycountyfl.org/ +Liberty,Montana,51,30,30051,county,Liberty County,https://co.liberty.mt.us/ +Liberty,Texas,291,48,48291,county,Liberty County,https://www.co.liberty.tx.us/ +Liberty,Georgia,179,13,13179,county,Liberty County,https://www.libertycountyga.com/ +Licking,Ohio,89,39,39089,county,Licking County,https://lickingcounty.gov/ +Limestone,Texas,293,48,48293,county,Limestone County,https://www.co.limestone.tx.us/ +Limestone,Alabama,83,1,1083,county,Limestone County,https://limestonecounty-al.gov/ +Lincoln,Idaho,63,16,16063,county,Lincoln County,http://lincolncountyid.us/ +Lincoln,Nevada,17,32,32017,county,Lincoln County,https://lincolncountynv.org/ +Lincoln,Missouri,113,29,29113,county,Lincoln County,https://lcmo.us/ +Lincoln,Maine,15,23,23015,county,Lincoln County,https://www.lincolncountymaine.me/ +Lincoln,Kansas,105,20,20105,county,Lincoln County,http://lincolncoks.com/ +Lincoln,Mississippi,85,28,28085,county,Lincoln County,http://www.golincolnms.com/ +Lincoln,Kentucky,137,21,21137,county,Lincoln County,http://www.lincolnky.com/ +Lincoln,Wyoming,23,56,56023,county,Lincoln County,https://www.lincolncountywy.gov/ +Lincoln,Washington,43,53,53043,county,Lincoln County,https://www.co.lincoln.wa.us/ +Lincoln,North Carolina,109,37,37109,county,Lincoln County,https://www.lincolncountync.gov/ +Lincoln,Minnesota,81,27,27081,county,Lincoln County,http://www.co.lincoln.mn.us/ +Lincoln,Montana,53,30,30053,county,Lincoln County,https://lincolncountymt.us/ +Lincoln,West Virginia,43,54,54043,county,Lincoln County,https://lincolncountywv.org/ +Lincoln,Oklahoma,81,40,40081,county,Lincoln County,https://lincolncountyok.org/ +Lincoln,Arkansas,79,5,5079,county,Lincoln County,https://local.arkansas.gov/local.php?agency=Lincoln%20County +Lincoln,New Mexico,27,35,35027,county,Lincoln County,https://www.lincolncountynm.gov/ +Lincoln,Georgia,181,13,13181,county,Lincoln County,https://www.lincolncountyga.com/ +Lincoln,Nebraska,111,31,31111,county,Lincoln County,https://lincolncountyne.gov/ +Lincoln,Louisiana,61,22,22061,parish,Lincoln Parish,https://www.lincolnparish.org/home +Lincoln,Colorado,73,8,8073,county,Lincoln County,https://lincolncounty.colorado.gov/ +Lincoln,Oregon,41,41,41041,county,Lincoln County,https://www.co.lincoln.or.us/ +Lincoln,Wisconsin,69,55,55069,county,Lincoln County,https://co.lincoln.wi.us/home +Lincoln,South Dakota,83,46,46083,county,Lincoln County,https://www.lincolncountysd.org/ +Lincoln,Tennessee,103,47,47103,county,Lincoln County,https://www.lincolncountytn.gov/ +Linn,Kansas,107,20,20107,county,Linn County,https://www.linncountyks.com/ +Linn,Iowa,113,19,19113,county,Linn County,https://www.linncountyiowa.gov/ +Linn,Oregon,43,41,41043,county,Linn County,https://www.linncountyor.gov/home +Linn,Missouri,115,29,29115,county,Linn County,https://linncomo.com/ +Lipscomb,Texas,295,48,48295,county,Lipscomb County,https://www.co.lipscomb.tx.us/ +Little River,Arkansas,81,5,5081,county,Little River County,https://www.arcounties.org/counties/little-river/ +Live Oak,Texas,297,48,48297,county,Live Oak County,https://www.co.live-oak.tx.us/ +Livingston,Kentucky,139,21,21139,county,Livingston County,http://livingstoncountyky.org/ +Livingston,Illinois,105,17,17105,county,Livingston County,https://www.livingstoncounty-il.org/wordpress/ +Livingston,New York,51,36,36051,county,Livingston County,https://www.livingstoncounty.us/ +Livingston,Louisiana,63,22,22063,parish,Livingston Parish,https://www.livingstonparishla.gov/home +Livingston,Michigan,93,26,26093,county,Livingston County,https://milivcounty.gov/ +Livingston,Missouri,117,29,29117,county,Livingston County,http://www.livingstoncountymo.com/ +Llano,Texas,299,48,48299,county,Llano County,https://www.co.llano.tx.us/ +Logan,Nebraska,113,31,31113,county,Logan County,https://logancounty.ne.gov/ +Logan,North Dakota,47,38,38047,county,Logan County,https://logancountynd.com/ +Logan,Illinois,107,17,17107,county,Logan County,https://logancountyil.gov/index.php?lang=en +Logan,Kansas,109,20,20109,county,Logan County,https://www.discoveroakley.com/101/Logan-County +Logan,Arkansas,83,5,5083,county,Logan County,https://www.logancountyark.org/ +Logan,Oklahoma,83,40,40083,county,Logan County,https://www.logancountyok.com/ +Logan,West Virginia,45,54,54045,county,Logan County,https://logancounty.wv.gov/ +Logan,Ohio,91,39,39091,county,Logan County,https://www.co.logan.oh.us/ +Logan,Kentucky,141,21,21141,county,Logan County,https://logancounty.ky.gov/ +Logan,Colorado,75,8,8075,county,Logan County,https://logancounty.colorado.gov/ +Long,Georgia,183,13,13183,county,Long County,https://www.longcountyga.gov/ +Lonoke,Arkansas,85,5,5085,county,Lonoke County,https://portal.arkansas.gov/counties/lonoke/ +Lorain,Ohio,93,39,39093,county,Lorain County,https://www.loraincountyohio.gov/ +Los Alamos,New Mexico,28,35,35028,county,Los Alamos County,https://www.losalamosnm.us/ +Los Angeles,California,37,6,6037,county,Los Angeles County,https://lacounty.gov/ +Loudon,Tennessee,105,47,47105,county,Loudon County,https://www.loudoncounty-tn.gov/ +Loudoun,Virginia,107,51,51107,county,Loudoun County,https://www.loudoun.gov/ +Louisa,Iowa,115,19,19115,county,Louisa County,https://louisacountyia.gov/ +Louisa,Virginia,109,51,51109,county,Louisa County,https://www.louisacounty.gov/ +Loup,Nebraska,115,31,31115,county,Loup County,https://loupcounty.nebraska.gov/ +Love,Oklahoma,85,40,40085,county,Love County,https://love.okcounties.org/ +Loving,Texas,301,48,48301,county,Loving County,https://www.co.loving.tx.us/ +Lower Connecticut River Valley,Connecticut,130,9,9130,planning region,Lower Connecticut River Valley Planning Region,https://www.rivercog.org/ +Lowndes,Georgia,185,13,13185,county,Lowndes County,https://www.lowndescounty.com/ +Lowndes,Mississippi,87,28,28087,county,Lowndes County,https://lowndescountyms.com/ +Lowndes,Alabama,85,1,1085,county,Lowndes County,https://www.lowndes-al.gov/ +Lubbock,Texas,303,48,48303,county,Lubbock County,https://www.lubbockcounty.gov/ +Lucas,Iowa,117,19,19117,county,Lucas County,https://lucascounty.iowa.gov/ +Lucas,Ohio,95,39,39095,county,Lucas County,https://www.co.lucas.oh.us/ +Luce,Michigan,95,26,26095,county,Luce County,https://www.lucecountymi.com/ +Lumpkin,Georgia,187,13,13187,county,Lumpkin County,https://www.lumpkincounty.gov/ +Luna,New Mexico,29,35,35029,county,Luna County,https://lunacountynm.us/ +Lunenburg,Virginia,111,51,51111,county,Lunenburg County,https://lunenburgva.gov/ +Luzerne,Pennsylvania,79,42,42079,county,Luzerne County,https://www.luzernecounty.org/ +Lycoming,Pennsylvania,81,42,42081,county,Lycoming County,https://www.lyco.org/ +Lyman,South Dakota,85,46,46085,county,Lyman County,http://lymancounty.org/ +Lynchburg,Virginia,680,51,51680,city,Lynchburg city,https://www.lynchburgva.gov/ +Lynn,Texas,305,48,48305,county,Lynn County,https://www.co.lynn.tx.us/ +Lyon,Iowa,119,19,19119,county,Lyon County,https://lyoncounty.iowa.gov/ +Lyon,Kentucky,143,21,21143,county,Lyon County,https://www.lyoncountyky.com/ +Lyon,Minnesota,83,27,27083,county,Lyon County,https://www.lyonco.org/ +Lyon,Kansas,111,20,20111,county,Lyon County,https://lyoncounty.org/index/ +Lyon,Nevada,19,32,32019,county,Lyon County,https://www.lyon-county.org/ +Mackinac,Michigan,97,26,26097,county,Mackinac County,https://www.mackinaccounty.net/ +Macomb,Michigan,99,26,26099,county,Macomb County,https://www.macombgov.org/ +Macon,Illinois,115,17,17115,county,Macon County,https://maconcounty.illinois.gov/ +Macon,North Carolina,113,37,37113,county,Macon County,https://maconnc.org/ +Macon,Tennessee,111,47,47111,county,Macon County,http://www.maconcountytn.gov/ +Macon,Missouri,121,29,29121,county,Macon County,https://www.maconcountymo.com/ +Macon,Alabama,87,1,1087,county,Macon County,https://maconalabama.com/ +Macon,Georgia,193,13,13193,county,Macon County,https://www.maconcountyga.gov/ +Macoupin,Illinois,117,17,17117,county,Macoupin County,https://macoupincountyil.gov/ +Madera,California,39,6,6039,county,Madera County,https://www.maderacounty.com/ +Madison,Tennessee,113,47,47113,county,Madison County,https://www.madisoncountytn.gov/ +Madison,Missouri,123,29,29123,county,Madison County,https://madisoncountymo.us/ +Madison,Ohio,97,39,39097,county,Madison County,https://www.co.madison.oh.us/ +Madison,Louisiana,65,22,22065,parish,Madison Parish,http://www.madisonparish.org/ +Madison,Iowa,121,19,19121,county,Madison County,https://madisoncounty.iowa.gov/ +Madison,Georgia,195,13,13195,county,Madison County,https://www.madisoncountyga.us/ +Madison,Montana,57,30,30057,county,Madison County,https://madisoncountymt.gov/ +Madison,Kentucky,151,21,21151,county,Madison County,https://madisoncountyky.us/ +Madison,Illinois,119,17,17119,county,Madison County,https://www.madisoncountyil.gov/ +Madison,Indiana,95,18,18095,county,Madison County,https://www.madisoncounty.in.gov/ +Madison,Alabama,89,1,1089,county,Madison County,https://www.madisoncountyal.gov/government/about-your-county +Madison,Mississippi,89,28,28089,county,Madison County,https://www.madison-co.com/ +Madison,Texas,313,48,48313,county,Madison County,https://www.co.madison.tx.us/ +Madison,North Carolina,115,37,37115,county,Madison County,https://www.madisoncountync.gov/ +Madison,Idaho,65,16,16065,county,Madison County,https://www.co.madison.id.us/ +Madison,New York,53,36,36053,county,Madison County,https://www.madisoncounty.ny.gov/ +Madison,Florida,79,12,12079,county,Madison County,https://madisoncountyfl.com/ +Madison,Virginia,113,51,51113,county,Madison County,https://www.madisonco.virginia.gov/ +Madison,Arkansas,87,5,5087,county,Madison County,https://madisoncogov.com/ +Madison,Nebraska,119,31,31119,county,Madison County,https://madisoncountyne.gov/ +Magoffin,Kentucky,153,21,21153,county,Magoffin County,https://magoffincounty.ky.gov/ +Mahaska,Iowa,123,19,19123,county,Mahaska County,https://www.mahaskacountyia.gov/ +Mahnomen,Minnesota,87,27,27087,county,Mahnomen County,https://co.mahnomen.mn.us/ +Mahoning,Ohio,99,39,39099,county,Mahoning County,https://www.mahoningcountyoh.gov/ +Major,Oklahoma,93,40,40093,county,Major County,https://oklahoma.gov/okdhs/library/resources/majorrd211.html +Malheur,Oregon,45,41,41045,county,Malheur County,https://www.malheurco.org/ +Manassas,Virginia,683,51,51683,city,Manassas city,https://www.manassasva.gov/ +Manassas Park,Virginia,685,51,51685,city,Manassas Park city,https://www.manassasparkva.gov/ +Manatee,Florida,81,12,12081,county,Manatee County,https://www.mymanatee.org/ +Manistee,Michigan,101,26,26101,county,Manistee County,https://www.manisteecountymi.gov/ +Manitowoc,Wisconsin,71,55,55071,county,Manitowoc County,https://manitowoccountywi.gov/ +Marathon,Wisconsin,73,55,55073,county,Marathon County,https://www.marathoncounty.gov/ +Marengo,Alabama,91,1,1091,county,Marengo County,https://www.marengocountyal.com/ +Maricopa,Arizona,13,4,4013,county,Maricopa County,https://www.maricopa.gov/ +Maries,Missouri,125,29,29125,county,Maries County,https://www.mariescountymo.gov/ +Marin,California,41,6,6041,county,Marin County,https://www.marincounty.org/ +Marinette,Wisconsin,75,55,55075,county,Marinette County,https://www.marinettecountywi.gov/ +Marion,Missouri,127,29,29127,county,Marion County,https://marioncountymo.com/ +Marion,West Virginia,49,54,54049,county,Marion County,http://www.marioncountywv.com/ +Marion,Iowa,125,19,19125,county,Marion County,https://www.marioncountyiowa.gov/ +Marion,Florida,83,12,12083,county,Marion County,https://www.marionfl.org/ +Marion,Alabama,93,1,1093,county,Marion County,https://marioncountyalabama.org/property.html +Marion,Tennessee,115,47,47115,county,Marion County,https://marioncountytn.net/ +Marion,Mississippi,91,28,28091,county,Marion County,http://www.marioncountyms.com/ +Marion,Georgia,197,13,13197,county,Marion County,https://www.marioncountyga.org/ +Marion,Arkansas,89,5,5089,county,Marion County,https://www.marioncounty.arkansas.gov/ +Marion,Illinois,121,17,17121,county,Marion County,https://marioncountyil.gov/ +Marion,Ohio,101,39,39101,county,Marion County,https://www.co.marion.oh.us/ +Marion,Kentucky,155,21,21155,county,Marion County,https://marioncounty.ky.gov/ +Marion,South Carolina,67,45,45067,county,Marion County,https://www.marionsc.org/ +Marion,Texas,315,48,48315,county,Marion County,https://www.co.marion.tx.us/ +Marion,Oregon,47,41,41047,county,Marion County,https://www.co.marion.or.us/ +Marion,Kansas,115,20,20115,county,Marion County,https://www.marioncoks.net/ +Marion,Indiana,97,18,18097,county,Marion County,https://www.in.gov/core/mylocal/marion_county.html +Mariposa,California,43,6,6043,county,Mariposa County,https://www.mariposacounty.org/ +Marlboro,South Carolina,69,45,45069,county,Marlboro County,https://marlborocounty.sc.gov/ +Marquette,Wisconsin,77,55,55077,county,Marquette County,https://www.co.marquette.wi.us/ +Marquette,Michigan,103,26,26103,county,Marquette County,https://www.co.marquette.mi.us/ +Marshall,Alabama,95,1,1095,county,Marshall County,https://www.marshallco.org/ +Marshall,Kansas,117,20,20117,county,Marshall County,https://ks283.cichosting.com/ +Marshall,West Virginia,51,54,54051,county,Marshall County,https://marshallcountywv.gov/ +Marshall,Oklahoma,95,40,40095,county,Marshall County,https://marshall.okcounties.org/ +Marshall,Kentucky,157,21,21157,county,Marshall County,https://www.marshallcountyky.gov/ +Marshall,Minnesota,89,27,27089,county,Marshall County,https://www.co.marshall.mn.us/ +Marshall,Iowa,127,19,19127,county,Marshall County,https://www.marshallcountyia.gov/ +Marshall,Illinois,123,17,17123,county,Marshall County,https://marshallcountyillinois.gov/ +Marshall,Mississippi,93,28,28093,county,Marshall County,http://www.marshall-county.com/ +Marshall,Indiana,99,18,18099,county,Marshall County,https://www.co.marshall.in.us/ +Marshall,Tennessee,117,47,47117,county,Marshall County,https://www.marshallcountytn.com/ +Marshall,South Dakota,91,46,46091,county,Marshall County,https://marshall.sdcounties.org/ +Martin,Minnesota,91,27,27091,county,Martin County,https://www.co.martin.mn.us/ +Martin,Florida,85,12,12085,county,Martin County,https://www.martin.fl.us/ +Martin,Texas,317,48,48317,county,Martin County,http://www.co.martin.tx.us/ +Martin,North Carolina,117,37,37117,county,Martin County,https://www.martincountyncgov.com/ +Martin,Kentucky,159,21,21159,county,Martin County,https://martincounty.ky.gov/ +Martin,Indiana,101,18,18101,county,Martin County,https://www.in.gov/counties/martin/ +Martinsville,Virginia,690,51,51690,city,Martinsville city,https://www.martinsville-va.gov/ +Mason,Washington,45,53,53045,county,Mason County,https://masoncountywa.gov/ +Mason,Illinois,125,17,17125,county,Mason County,https://masoncountyil.gov/ +Mason,West Virginia,53,54,54053,county,Mason County,https://masoncountywv.gov/ +Mason,Michigan,105,26,26105,county,Mason County,https://www.masoncounty.net/ +Mason,Kentucky,161,21,21161,county,Mason County,https://masoncountykentucky.us/ +Mason,Texas,319,48,48319,county,Mason County,https://www.co.mason.tx.us/ +Massac,Illinois,127,17,17127,county,Massac County,https://www.ilsos.gov/departments/archives/IRAD/massac.html +Matagorda,Texas,321,48,48321,county,Matagorda County,https://www.co.matagorda.tx.us/ +Mathews,Virginia,115,51,51115,county,Mathews County,https://www.mathewscountyva.gov/ +Maury,Tennessee,119,47,47119,county,Maury County,https://www.maurycounty-tn.gov/ +Maverick,Texas,323,48,48323,county,Maverick County,https://co.maverick.tx.us/ +Mayes,Oklahoma,97,40,40097,county,Mayes County,https://mayes.okcounties.org/ +McClain,Oklahoma,87,40,40087,county,McClain County,https://mcclain-co-ok.us/ +McCone,Montana,55,30,30055,county,McCone County,https://mcconecountymt.com/ +McCook,South Dakota,87,46,46087,county,McCook County,https://www.mccookcountysd.com/ +McCormick,South Carolina,65,45,45065,county,McCormick County,https://www.mccormickcountysc.org/ +McCracken,Kentucky,145,21,21145,county,McCracken County,https://mccrackencountyky.gov/ +McCreary,Kentucky,147,21,21147,county,McCreary County,https://mccrearycounty.com/ +McCulloch,Texas,307,48,48307,county,McCulloch County,https://www.co.mcculloch.tx.us/ +McCurtain,Oklahoma,89,40,40089,county,McCurtain County,https://mccurtain.okcounties.org/ +McDonald,Missouri,119,29,29119,county,McDonald County,https://mcdonaldcountymo.gov/ +McDonough,Illinois,109,17,17109,county,McDonough County,http://mcg.mcdonough.il.us/ +McDowell,West Virginia,47,54,54047,county,McDowell County,https://www.wv.gov/local/Pages/counties.aspx?county=mcdowell +McDowell,North Carolina,111,37,37111,county,McDowell County,https://www.mcdowellgov.com/ +McDuffie,Georgia,189,13,13189,county,McDuffie County,https://www.thomson-mcduffie.gov/home +McHenry,Illinois,111,17,17111,county,McHenry County,https://www.mchenrycountyil.gov/ +McHenry,North Dakota,49,38,38049,county,McHenry County,https://www.mchenrycountynd.com/ +McIntosh,Oklahoma,91,40,40091,county,McIntosh County,https://mcintoshcountyok.gov/ +McIntosh,Georgia,191,13,13191,county,McIntosh County,https://www.mcintoshcountyga.com/ +McIntosh,North Dakota,51,38,38051,county,McIntosh County,https://www.mcintoshnd.com/ +McKean,Pennsylvania,83,42,42083,county,McKean County,https://www.mckeancountypa.gov/ +McKenzie,North Dakota,53,38,38053,county,McKenzie County,https://county.mckenziecounty.net/ +McKinley,New Mexico,31,35,35031,county,McKinley County,https://www.co.mckinley.nm.us/ +McLean,Illinois,113,17,17113,county,McLean County,https://www.mcleancountyil.gov/ +McLean,Kentucky,149,21,21149,county,McLean County,https://www.mcleancounty.ky.gov/ +McLean,North Dakota,55,38,38055,county,McLean County,https://www.mcleancountynd.gov/ +McLennan,Texas,309,48,48309,county,McLennan County,https://www.co.mclennan.tx.us/ +McLeod,Minnesota,85,27,27085,county,McLeod County,http://www.mcleodcountymn.gov/ +McMinn,Tennessee,107,47,47107,county,McMinn County,https://www.mcminncountytn.gov/ +McMullen,Texas,311,48,48311,county,McMullen County,https://mcmullencounty.org/ +McNairy,Tennessee,109,47,47109,county,McNairy County,https://www.mcnairycountytn.org/ +McPherson,Kansas,113,20,20113,county,McPherson County,https://www.mcphersoncountyks.us/ +McPherson,South Dakota,89,46,46089,county,McPherson County,https://mcpherson.sdcounties.org/ +McPherson,Nebraska,117,31,31117,county,McPherson County,https://mcphersoncounty.ne.gov/ +Meade,Kansas,119,20,20119,county,Meade County,https://www.meadeco.org/ +Meade,South Dakota,93,46,46093,county,Meade County,https://www.meadecounty.org/ +Meade,Kentucky,163,21,21163,county,Meade County,https://meadeky.com/ +Meagher,Montana,59,30,30059,county,Meagher County,https://meagherco.com/ +Mecklenburg,Virginia,117,51,51117,county,Mecklenburg County,https://www.mecklenburgva.com/ +Mecklenburg,North Carolina,119,37,37119,county,Mecklenburg County,https://www.mecknc.gov/ +Mecosta,Michigan,107,26,26107,county,Mecosta County,https://www.mecostacounty.org/ +Medina,Texas,325,48,48325,county,Medina County,https://www.medinacountytexas.org/ +Medina,Ohio,103,39,39103,county,Medina County,https://www.medinaco.org/ +Meeker,Minnesota,93,27,27093,county,Meeker County,https://www.co.meeker.mn.us/ +Meigs,Tennessee,121,47,47121,county,Meigs County,https://www.meigscounty.org/ +Meigs,Ohio,105,39,39105,county,Meigs County,https://www.meigscountyohio.com/ +Mellette,South Dakota,95,46,46095,county,Mellette County,https://ujs.sd.gov/Sixth_Circuit/Links/Counties.aspx?SD8NTJQCeKtPiCf7fX3Do03crGiheziIijVZa8oR7nE%3D +Menard,Illinois,129,17,17129,county,Menard County,https://menardcountyil.com/contact-us/ +Menard,Texas,327,48,48327,county,Menard County,https://www.co.menard.tx.us/ +Mendocino,California,45,6,6045,county,Mendocino County,https://www.mendocinocounty.org/ +Menifee,Kentucky,165,21,21165,county,Menifee County,https://menifeecounty.ky.gov/ +Menominee,Wisconsin,78,55,55078,county,Menominee County,https://www.co.menominee.wi.us/ +Menominee,Michigan,109,26,26109,county,Menominee County,https://www.menomineecounty.com/ +Merced,California,47,6,6047,county,Merced County,https://www.countyofmerced.com/ +Mercer,North Dakota,57,38,38057,county,Mercer County,http://www.mercercountynd.com/ +Mercer,New Jersey,21,34,34021,county,Mercer County,https://www.mercercounty.org/ +Mercer,Illinois,131,17,17131,county,Mercer County,https://www.mercercountyil.org/ +Mercer,Kentucky,167,21,21167,county,Mercer County,https://mercercounty.ky.gov/Pages/index.aspx +Mercer,Pennsylvania,85,42,42085,county,Mercer County,https://www.mercercountypa.gov/ +Mercer,Missouri,129,29,29129,county,Mercer County,https://www.mocounties.com/mercer-county +Mercer,West Virginia,55,54,54055,county,Mercer County,https://www.wv.gov/local/Pages/counties.aspx?county=mercer +Mercer,Ohio,107,39,39107,county,Mercer County,https://www.mercercountyohio.org/ +Meriwether,Georgia,199,13,13199,county,Meriwether County,https://meriwethercountyga.gov/ +Merrick,Nebraska,121,31,31121,county,Merrick County,https://merrickcounty.ne.gov/ +Merrimack,New Hampshire,13,33,33013,county,Merrimack County,https://www.merrimackcounty.net/ +Mesa,Colorado,77,8,8077,county,Mesa County,https://www.mesacounty.us/ +Metcalfe,Kentucky,169,21,21169,county,Metcalfe County,https://metcalfecounty.ky.gov/ +Miami,Kansas,121,20,20121,county,Miami County,https://www.miamicountyks.org/ +Miami,Ohio,109,39,39109,county,Miami County,https://www.co.miami.oh.us/ +Miami,Indiana,103,18,18103,county,Miami County,https://www.miamicountyin.gov/ +Miami-Dade,Florida,86,12,12086,county,Miami-Dade County,https://www.miamidade.gov/ +Middlesex,Massachusetts,17,25,25017,county,Middlesex County,https://www.mass.gov/locations/middlesex-county-superior-court +Middlesex,Virginia,119,51,51119,county,Middlesex County,https://www.co.middlesex.va.us/ +Middlesex,New Jersey,23,34,34023,county,Middlesex County,https://www.middlesexcountynj.gov/ +Midland,Texas,329,48,48329,county,Midland County,https://www.co.midland.tx.us/ +Midland,Michigan,111,26,26111,county,Midland County,https://midlandcountymi.gov/ +Mifflin,Pennsylvania,87,42,42087,county,Mifflin County,https://mifflinco.org/ +Milam,Texas,331,48,48331,county,Milam County,https://www.milamcounty.net/ +Millard,Utah,27,49,49027,county,Millard County,https://millardcounty.org/ +Mille Lacs,Minnesota,95,27,27095,county,Mille Lacs County,https://www.millelacs.mn.gov/ +Miller,Georgia,201,13,13201,county,Miller County,https://www.millercountyga.gov/ +Miller,Missouri,131,29,29131,county,Miller County,https://www.millercountymissouri.org/ +Miller,Arkansas,91,5,5091,county,Miller County,https://millercountyar.com/ +Mills,Texas,333,48,48333,county,Mills County,https://www.millscountytx.gov/ +Mills,Iowa,129,19,19129,county,Mills County,https://www.millscountyiowa.gov/ +Milwaukee,Wisconsin,79,55,55079,county,Milwaukee County,http://www.county.milwaukee.gov/ +Miner,South Dakota,97,46,46097,county,Miner County,https://www.minercountysd.org/ +Mineral,West Virginia,57,54,54057,county,Mineral County,https://www.mineralwv.org/ +Mineral,Montana,61,30,30061,county,Mineral County,https://co.mineral.mt.us/ +Mineral,Nevada,21,32,32021,county,Mineral County,http://mineralcountynv.us/ +Mineral,Colorado,79,8,8079,county,Mineral County,https://mineralcounty.colorado.gov/ +Mingo,West Virginia,59,54,54059,county,Mingo County,https://www.wv.gov/local/Pages/counties.aspx?county=mingo +Minidoka,Idaho,67,16,16067,county,Minidoka County,https://www.minidoka.id.us/ +Minnehaha,South Dakota,99,46,46099,county,Minnehaha County,https://www.minnehahacounty.gov/ +Missaukee,Michigan,113,26,26113,county,Missaukee County,https://www.missaukee.org/ +Mississippi,Arkansas,93,5,5093,county,Mississippi County,https://www.mississippicountyar.org/ +Mississippi,Missouri,133,29,29133,county,Mississippi County,https://misscomo.net/ +Missoula,Montana,63,30,30063,county,Missoula County,https://www.missoulacounty.us/ +Mitchell,Georgia,205,13,13205,county,Mitchell County,https://www.mitchellcountyga.net/ +Mitchell,North Carolina,121,37,37121,county,Mitchell County,https://www.mitchellcountync.gov/ +Mitchell,Iowa,131,19,19131,county,Mitchell County,https://mitchellcounty.iowa.gov/ +Mitchell,Kansas,123,20,20123,county,Mitchell County,https://www.mitchellcountykansas.com/ +Mitchell,Texas,335,48,48335,county,Mitchell County,https://www.co.mitchell.tx.us/ +Mobile,Alabama,97,1,1097,county,Mobile County,https://www.mobilecountyal.gov/ +Modoc,California,49,6,6049,county,Modoc County,https://www.co.modoc.ca.us/ +Moffat,Colorado,81,8,8081,county,Moffat County,https://moffatcounty.colorado.gov/ +Mohave,Arizona,15,4,4015,county,Mohave County,https://www.mohave.gov/ +Moniteau,Missouri,135,29,29135,county,Moniteau County,https://www.mocounties.com/moniteau-county +Monmouth,New Jersey,25,34,34025,county,Monmouth County,http://visitmonmouth.com/ +Mono,California,51,6,6051,county,Mono County,https://www.monocounty.ca.gov/home +Monona,Iowa,133,19,19133,county,Monona County,https://mononacounty.iowa.gov/ +Monongalia,West Virginia,61,54,54061,county,Monongalia County,https://www.monongaliacounty.gov/ +Monroe,West Virginia,63,54,54063,county,Monroe County,https://www.monroecountywv.gov/ +Monroe,Mississippi,95,28,28095,county,Monroe County,https://www.monroems.com/ +Monroe,Tennessee,123,47,47123,county,Monroe County,https://monroetn.com/ +Monroe,Michigan,115,26,26115,county,Monroe County,https://www.co.monroe.mi.us/ +Monroe,Alabama,99,1,1099,county,Monroe County,https://www.monroecountyonline.com/ +Monroe,New York,55,36,36055,county,Monroe County,https://www.monroecounty.gov/ +Monroe,Wisconsin,81,55,55081,county,Monroe County,https://www.co.monroe.wi.us/ +Monroe,Iowa,135,19,19135,county,Monroe County,https://monroecounty.iowa.gov/ +Monroe,Florida,87,12,12087,county,Monroe County,https://www.monroecounty-fl.gov/ +Monroe,Georgia,207,13,13207,county,Monroe County,https://www.monroecoga.org/ +Monroe,Indiana,105,18,18105,county,Monroe County,https://www.co.monroe.in.us/ +Monroe,Missouri,137,29,29137,county,Monroe County,http://www.monroecountymo.org/ +Monroe,Pennsylvania,89,42,42089,county,Monroe County,https://www.monroecountypa.gov/ +Monroe,Ohio,111,39,39111,county,Monroe County,https://www.monroecountyohio.net/ +Monroe,Arkansas,95,5,5095,county,Monroe County,https://local.arkansas.gov/local.php?agency=Monroe%20County +Monroe,Kentucky,171,21,21171,county,Monroe County,https://monroecounty.ky.gov/Elected-Officials/Pages/County-Officials.aspx +Monroe,Illinois,133,17,17133,county,Monroe County,https://monroecountyil.gov/ +Montague,Texas,337,48,48337,county,Montague County,https://www.co.montague.tx.us/ +Montcalm,Michigan,117,26,26117,county,Montcalm County,https://www.montcalm.us/ +Monterey,California,53,6,6053,county,Monterey County,https://www.co.monterey.ca.us/ +Montezuma,Colorado,83,8,8083,county,Montezuma County,https://montezumacounty.org/ +Montgomery,Mississippi,97,28,28097,county,Montgomery County,http://www.montgomerycountyms.com/ +Montgomery,Illinois,135,17,17135,county,Montgomery County,https://montgomeryco.com/ +Montgomery,Pennsylvania,91,42,42091,county,Montgomery County,https://www.montgomerycountypa.gov/ +Montgomery,North Carolina,123,37,37123,county,Montgomery County,https://www.montgomerycountync.com/ +Montgomery,Arkansas,97,5,5097,county,Montgomery County,https://local.arkansas.gov/local.php?agency=Montgomery%20County +Montgomery,Virginia,121,51,51121,county,Montgomery County,https://montva.com/ +Montgomery,Iowa,137,19,19137,county,Montgomery County,https://www.montgomerycountyia.gov/ +Montgomery,Alabama,101,1,1101,county,Montgomery County,https://www.mc-ala.org/ +Montgomery,New York,57,36,36057,county,Montgomery County,https://www.co.montgomery.ny.us/ +Montgomery,Tennessee,125,47,47125,county,Montgomery County,https://mcgtn.org/ +Montgomery,Missouri,139,29,29139,county,Montgomery County,http://mcmo.us/ +Montgomery,Texas,339,48,48339,county,Montgomery County,https://www.mctx.org/ +Montgomery,Indiana,107,18,18107,county,Montgomery County,https://www.montgomerycounty.in.gov/ +Montgomery,Georgia,209,13,13209,county,Montgomery County,https://www.montcoga.gov/ +Montgomery,Ohio,113,39,39113,county,Montgomery County,https://www.mcohio.org/ +Montgomery,Kentucky,173,21,21173,county,Montgomery County,https://montgomerycounty.ky.gov/Pages/default.aspx +Montgomery,Maryland,31,24,24031,county,Montgomery County,https://www.montgomerycountymd.gov/ +Montgomery,Kansas,125,20,20125,county,Montgomery County,https://www.mgcountyks.org/ +Montmorency,Michigan,119,26,26119,county,Montmorency County,https://www.montcounty.org/ +Montour,Pennsylvania,93,42,42093,county,Montour County,https://montourcounty.gov/ +Montrose,Colorado,85,8,8085,county,Montrose County,https://www.montrosecounty.net/ +Moody,South Dakota,101,46,46101,county,Moody County,https://www.moodycounty.net/ +Moore,Tennessee,127,47,47127,county,Moore County,https://metromoorecounty.org/ +Moore,North Carolina,125,37,37125,county,Moore County,https://www.moorecountync.gov/ +Moore,Texas,341,48,48341,county,Moore County,https://www.co.moore.tx.us/ +Mora,New Mexico,33,35,35033,county,Mora County,https://countyofmora.com/ +Morehouse,Louisiana,67,22,22067,parish,Morehouse Parish,https://www.louisiana.gov/local-louisiana/morehouse-parish +Morgan,Indiana,109,18,18109,county,Morgan County,https://morgancounty.in.gov/ +Morgan,Tennessee,129,47,47129,county,Morgan County,https://www.morgancountytn.gov/ +Morgan,Utah,29,49,49029,county,Morgan County,https://www.morgancountyutah.gov/ +Morgan,Georgia,211,13,13211,county,Morgan County,https://www.morgancountyga.gov/183/Government?nid=183&mobile=ON +Morgan,Kentucky,175,21,21175,county,Morgan County,https://morgancounty.ky.gov/ +Morgan,Alabama,103,1,1103,county,Morgan County,https://morgancounty-al.gov/ +Morgan,West Virginia,65,54,54065,county,Morgan County,https://morgancountywv.gov/ +Morgan,Colorado,87,8,8087,county,Morgan County,https://morgancounty.colorado.gov/ +Morgan,Missouri,141,29,29141,county,Morgan County,https://www.morgancountymo.gov/ +Morgan,Ohio,115,39,39115,county,Morgan County,https://www.morgancounty-oh.gov/ +Morgan,Illinois,137,17,17137,county,Morgan County,https://morgancounty-il.com/wp/ +Morrill,Nebraska,123,31,31123,county,Morrill County,https://www.morrillcountyne.gov/ +Morris,Kansas,127,20,20127,county,Morris County,https://www.morriscountyks.org/ +Morris,New Jersey,27,34,34027,county,Morris County,https://www.morriscountynj.gov/Home +Morris,Texas,343,48,48343,county,Morris County,https://www.co.morris.tx.us/ +Morrison,Minnesota,97,27,27097,county,Morrison County,https://www.co.morrison.mn.us/ +Morrow,Ohio,117,39,39117,county,Morrow County,https://morrowcountyohio.gov/ +Morrow,Oregon,49,41,41049,county,Morrow County,https://www.co.morrow.or.us/home +Morton,Kansas,129,20,20129,county,Morton County,https://www.mtcoks.com/ +Morton,North Dakota,59,38,38059,county,Morton County,https://www.mortonnd.org/ +Motley,Texas,345,48,48345,county,Motley County,https://www.co.motley.tx.us/ +Moultrie,Illinois,139,17,17139,county,Moultrie County,https://www.moultriecountyil.gov/ +Mountrail,North Dakota,61,38,38061,county,Mountrail County,http://www.co.mountrail.nd.us/ +Mower,Minnesota,99,27,27099,county,Mower County,https://www.co.mower.mn.us/ +Muhlenberg,Kentucky,177,21,21177,county,Muhlenberg County,https://muhlenbergcounty.ky.gov/Pages/index.aspx +Multnomah,Oregon,51,41,41051,county,Multnomah County,https://www.multco.us/ +Murray,Oklahoma,99,40,40099,county,Murray County,https://oklahoma.gov/okdhs/library/resources/murrayrd211.html +Murray,Minnesota,101,27,27101,county,Murray County,https://www.murraycountymn.com/ +Murray,Georgia,213,13,13213,county,Murray County,https://www.murraycountyga.org/ +Muscatine,Iowa,139,19,19139,county,Muscatine County,https://www.muscatinecountyiowa.gov/ +Muscogee,Georgia,215,13,13215,county,Muscogee County,https://www.columbusga.gov/ +Muskegon,Michigan,121,26,26121,county,Muskegon County,https://co.muskegon.mi.us/ +Muskingum,Ohio,119,39,39119,county,Muskingum County,https://www.muskingumcountyoh.gov/ +Muskogee,Oklahoma,101,40,40101,county,Muskogee County,https://muskogee.okcounties.org/ +Musselshell,Montana,65,30,30065,county,Musselshell County,https://musselshellcounty.org/ +Nacogdoches,Texas,347,48,48347,county,Nacogdoches County,https://www.co.nacogdoches.tx.us/ +Nance,Nebraska,125,31,31125,county,Nance County,https://nancecountyne.gov/ +Nantucket,Massachusetts,19,25,25019,county,Nantucket County,https://www.nantucket-ma.gov/ +Napa,California,55,6,6055,county,Napa County,https://www.countyofnapa.org/ +Nash,North Carolina,127,37,37127,county,Nash County,https://nashcountync.gov/ +Nassau,Florida,89,12,12089,county,Nassau County,https://www.nassaucountyfl.com/ +Nassau,New York,59,36,36059,county,Nassau County,https://www.nassaucountyny.gov/ +Natchitoches,Louisiana,69,22,22069,parish,Natchitoches Parish,https://npgov.org/ +Natrona,Wyoming,25,56,56025,county,Natrona County,https://www.natronacounty-wy.gov/ +Naugatuck Valley,Connecticut,140,9,9140,planning region,Naugatuck Valley Planning Region,https://nvcogct.gov/ +Navajo,Arizona,17,4,4017,county,Navajo County,https://navajocountyaz.gov/ +Navarro,Texas,349,48,48349,county,Navarro County,https://www.co.navarro.tx.us/ +Nelson,Virginia,125,51,51125,county,Nelson County,https://www.nelsoncounty.com/ +Nelson,North Dakota,63,38,38063,county,Nelson County,https://www.nelsonco.org/ +Nelson,Kentucky,179,21,21179,county,Nelson County,https://nelsoncountyky.gov/ +Nemaha,Kansas,131,20,20131,county,Nemaha County,https://www.nmcoks.us/ +Nemaha,Nebraska,127,31,31127,county,Nemaha County,https://nemahacounty.ne.gov/ +Neosho,Kansas,133,20,20133,county,Neosho County,https://www.neoshocountyks.org/ +Neshoba,Mississippi,99,28,28099,county,Neshoba County,http://www.neshobacounty.net/ +Ness,Kansas,135,20,20135,county,Ness County,https://www.nesscountyks.com/ +Nevada,Arkansas,99,5,5099,county,Nevada County,https://local.arkansas.gov/local.php?agency=Nevada%20County +Nevada,California,57,6,6057,county,Nevada County,https://www.nevadacountyca.gov/ +New Castle,Delaware,3,10,10003,county,New Castle County,https://www.newcastlede.gov/ +New Hanover,North Carolina,129,37,37129,county,New Hanover County,https://www.nhcgov.com/ +New Kent,Virginia,127,51,51127,county,New Kent County,https://www.co.new-kent.va.us/ +New Madrid,Missouri,143,29,29143,county,New Madrid County,http://www.new-madrid.mo.us/116/New-Madrid-County +New York,New York,61,36,36061,county,New York County,https://www.ny.gov/counties +Newaygo,Michigan,123,26,26123,county,Newaygo County,https://www.newaygocountymi.gov/ +Newberry,South Carolina,71,45,45071,county,Newberry County,https://www.newberrycounty.gov/ +Newport,Rhode Island,5,44,44005,county,Newport County,https://www.cityofnewport.com/ +Newport News,Virginia,700,51,51700,city,Newport News city,https://www.nnva.gov/ +Newton,Indiana,111,18,18111,county,Newton County,https://www.newtoncounty.in.gov/ +Newton,Missouri,145,29,29145,county,Newton County,https://www.newtoncountymo.com/ +Newton,Mississippi,101,28,28101,county,Newton County,https://www.newtoncountyms.net/ +Newton,Georgia,217,13,13217,county,Newton County,https://www.co.newton.ga.us/ +Newton,Texas,351,48,48351,county,Newton County,https://www.co.newton.tx.us/ +Newton,Arkansas,101,5,5101,county,Newton County,https://local.arkansas.gov/local.php?agency=newton%20County +Nez Perce,Idaho,69,16,16069,county,Nez Perce County,https://www.co.nezperce.id.us/ +Niagara,New York,63,36,36063,county,Niagara County,https://www.niagaracounty.com/ +Nicholas,West Virginia,67,54,54067,county,Nicholas County,https://www.nicholascountywv.org/ +Nicholas,Kentucky,181,21,21181,county,Nicholas County,https://nicholascounty.ky.gov/ +Nicollet,Minnesota,103,27,27103,county,Nicollet County,https://www.co.nicollet.mn.us/ +Niobrara,Wyoming,27,56,56027,county,Niobrara County,https://niobraracounty.org/ +Noble,Ohio,121,39,39121,county,Noble County,https://noblecountyohio.gov/ +Noble,Oklahoma,103,40,40103,county,Noble County,https://www.noblecountyok.com/ +Noble,Indiana,113,18,18113,county,Noble County,http://nobleco.squarespace.com/ +Nobles,Minnesota,105,27,27105,county,Nobles County,https://www.co.nobles.mn.us/ +Nodaway,Missouri,147,29,29147,county,Nodaway County,https://nodawaycountymo.us/ +Nolan,Texas,353,48,48353,county,Nolan County,https://www.co.nolan.tx.us/ +Norfolk,Massachusetts,21,25,25021,county,Norfolk County,https://www.norfolkcounty.org/ +Norfolk,Virginia,710,51,51710,city,Norfolk city,https://www.norfolk.gov/ +Norman,Minnesota,107,27,27107,county,Norman County,https://www.co.norman.mn.us/ +Northampton,Virginia,131,51,51131,county,Northampton County,https://www.co.northampton.va.us/ +Northampton,Pennsylvania,95,42,42095,county,Northampton County,https://www.northamptoncounty.org/Pages/default.aspx +Northampton,North Carolina,131,37,37131,county,Northampton County,http://www.northamptonnc.com/ +Northeastern Connecticut,Connecticut,150,9,9150,planning region,Northeastern Connecticut Planning Region,"https://en.wikipedia.org/wiki/Northeastern_Connecticut_Planning_Region,_Connecticut" +Northumberland,Pennsylvania,97,42,42097,county,Northumberland County,https://www.norrycopa.net/ +Northumberland,Virginia,133,51,51133,county,Northumberland County,https://www.co.northumberland.va.us/home +Northwest Hills,Connecticut,160,9,9160,planning region,Northwest Hills Planning Region,https://northwesthillscog.org/ +Norton,Virginia,720,51,51720,city,Norton city,https://www.nortonva.gov/ +Norton,Kansas,137,20,20137,county,Norton County,https://www.nortoncountyks.gov/ +Nottoway,Virginia,135,51,51135,county,Nottoway County,https://nottoway.org/ +Nowata,Oklahoma,105,40,40105,county,Nowata County,https://oklahoma.gov/okdhs/library/resources/nowatard211.html +Noxubee,Mississippi,103,28,28103,county,Noxubee County,https://noxubeealliance.com/quality-of-life-noxubee-mississipp/noxubee-county-government/ +Nuckolls,Nebraska,129,31,31129,county,Nuckolls County,https://nuckollscounty.ne.gov/ +Nueces,Texas,355,48,48355,county,Nueces County,https://www.nuecesco.com/ +Nye,Nevada,23,32,32023,county,Nye County,https://www.nyecountynv.gov/ +O'Brien,Iowa,141,19,19141,county,O'Brien County,https://obriencounty.iowa.gov/ +Oakland,Michigan,125,26,26125,county,Oakland County,https://www.oakgov.com/ +Obion,Tennessee,131,47,47131,county,Obion County,https://www.obioncountytn.gov/ +Ocean,New Jersey,29,34,34029,county,Ocean County,https://www.co.ocean.nj.us/ +Oceana,Michigan,127,26,26127,county,Oceana County,https://oceana.mi.us/ +Ochiltree,Texas,357,48,48357,county,Ochiltree County,https://www.co.ochiltree.tx.us/ +Oconee,Georgia,219,13,13219,county,Oconee County,https://www.oconeecounty.com/ +Oconee,South Carolina,73,45,45073,county,Oconee County,https://oconeesc.com/ +Oconto,Wisconsin,83,55,55083,county,Oconto County,https://www.co.oconto.wi.us/ +Ogemaw,Michigan,129,26,26129,county,Ogemaw County,https://www.ocmi.us/ +Oglala Lakota,South Dakota,102,46,46102,county,Oglala Lakota County,https://oglalalakota.sdcounties.org/ +Ogle,Illinois,141,17,17141,county,Ogle County,https://www.oglecountyil.gov/ +Oglethorpe,Georgia,221,13,13221,county,Oglethorpe County,https://www.oglethorpecountyga.gov/ +Ohio,Indiana,115,18,18115,county,Ohio County,https://ohiocountyin.gov/ +Ohio,West Virginia,69,54,54069,county,Ohio County,https://www.ohiocountywv.gov/ +Ohio,Kentucky,183,21,21183,county,Ohio County,https://ohiocounty.ky.gov/ +Okaloosa,Florida,91,12,12091,county,Okaloosa County,https://myokaloosa.com/ +Okanogan,Washington,47,53,53047,county,Okanogan County,https://www.okanogancounty.org/ +Okeechobee,Florida,93,12,12093,county,Okeechobee County,https://www.co.okeechobee.fl.us/ +Okfuskee,Oklahoma,107,40,40107,county,Okfuskee County,https://oklahoma.gov/okdhs/library/resources/okfuskeerd211.html +Oklahoma,Oklahoma,109,40,40109,county,Oklahoma County,https://www.oklahomacounty.org/ +Okmulgee,Oklahoma,111,40,40111,county,Okmulgee County,http://okmulgeecounty.net/ +Oktibbeha,Mississippi,105,28,28105,county,Oktibbeha County,https://www.oktibbehacountyms.org/ +Oldham,Kentucky,185,21,21185,county,Oldham County,https://www.oldhamcountyky.gov/ +Oldham,Texas,359,48,48359,county,Oldham County,https://www.co.oldham.tx.us/ +Oliver,North Dakota,65,38,38065,county,Oliver County,https://olivercountynd.org/ +Olmsted,Minnesota,109,27,27109,county,Olmsted County,https://www.olmstedcounty.gov/ +Oneida,Wisconsin,85,55,55085,county,Oneida County,https://www.co.oneida.wi.us/ +Oneida,New York,65,36,36065,county,Oneida County,https://ocgov.net/ +Oneida,Idaho,71,16,16071,county,Oneida County,https://www.oneidaid.us/ +Onondaga,New York,67,36,36067,county,Onondaga County,http://www.ongov.net/ +Onslow,North Carolina,133,37,37133,county,Onslow County,https://www.onslowcountync.gov/ +Ontario,New York,69,36,36069,county,Ontario County,https://ontariocountyny.gov/ +Ontonagon,Michigan,131,26,26131,county,Ontonagon County,https://ontonagoncounty.org/ +Orange,Florida,95,12,12095,county,Orange County,https://www.orangecountyfl.net/ +Orange,Indiana,117,18,18117,county,Orange County,http://gov.orangecounty59.us/ +Orange,California,59,6,6059,county,Orange County,https://www.ocgov.com/ +Orange,New York,71,36,36071,county,Orange County,https://www.orangecountygov.com/ +Orange,Texas,361,48,48361,county,Orange County,https://www.co.orange.tx.us/ +Orange,North Carolina,135,37,37135,county,Orange County,https://www.orangecountync.gov/ +Orange,Vermont,17,50,50017,county,Orange County,http://vermont.gov/ +Orange,Virginia,137,51,51137,county,Orange County,https://orangecountyva.gov/ +Orangeburg,South Carolina,75,45,45075,county,Orangeburg County,https://www.orangeburgcounty.org/ +Oregon,Missouri,149,29,29149,county,Oregon County,https://www.mocounties.com/oregon-county +Orleans,Vermont,19,50,50019,county,Orleans County,http://bgs.vermont.gov/facilites/east/orleanscourt +Orleans,Louisiana,71,22,22071,parish,Orleans Parish,https://www.louisiana.gov/local-louisiana/orleans-parish +Orleans,New York,73,36,36073,county,Orleans County,https://www.orleanscountyny.gov/ +Osage,Kansas,139,20,20139,county,Osage County,https://www.osageco.org/ +Osage,Missouri,151,29,29151,county,Osage County,http://osagecountygov.com/ +Osage,Oklahoma,113,40,40113,county,Osage County,https://osage.okcounties.org/ +Osborne,Kansas,141,20,20141,county,Osborne County,https://www.osbornecounty.org/ +Osceola,Iowa,143,19,19143,county,Osceola County,https://osceolacountyia.gov/ +Osceola,Michigan,133,26,26133,county,Osceola County,https://www.osceola-county.org/ +Osceola,Florida,97,12,12097,county,Osceola County,https://www.osceola.org/ +Oscoda,Michigan,135,26,26135,county,Oscoda County,https://www.oscodacountymi.com/ +Oswego,New York,75,36,36075,county,Oswego County,https://www.oswegocounty.com/ +Otero,Colorado,89,8,8089,county,Otero County,https://oterocounty.colorado.gov/ +Otero,New Mexico,35,35,35035,county,Otero County,https://co.otero.nm.us/ +Otoe,Nebraska,131,31,31131,county,Otoe County,https://otoecountyne.gov/ +Otsego,New York,77,36,36077,county,Otsego County,https://www.otsegocounty.com/ +Otsego,Michigan,137,26,26137,county,Otsego County,https://www.otsegocountymi.gov/ +Ottawa,Kansas,143,20,20143,county,Ottawa County,http://www.ottawacounty.org/ +Ottawa,Ohio,123,39,39123,county,Ottawa County,https://www.co.ottawa.oh.us/ +Ottawa,Michigan,139,26,26139,county,Ottawa County,https://www.miottawa.org/ +Ottawa,Oklahoma,115,40,40115,county,Ottawa County,https://ottawa.okcounties.org/ +Otter Tail,Minnesota,111,27,27111,county,Otter Tail County,https://ottertailcounty.gov/ +Ouachita,Arkansas,103,5,5103,county,Ouachita County,https://local.arkansas.gov/local.php?agency=Ouachita%20County +Ouachita,Louisiana,73,22,22073,parish,Ouachita Parish,https://www.louisiana.gov/local-louisiana/ouachita-parish +Ouray,Colorado,91,8,8091,county,Ouray County,https://ouraycountyco.gov/ +Outagamie,Wisconsin,87,55,55087,county,Outagamie County,https://www.outagamie.org/ +Overton,Tennessee,133,47,47133,county,Overton County,https://overtoncountytn.gov/ +Owen,Kentucky,187,21,21187,county,Owen County,https://www.owencountyky.us/ +Owen,Indiana,119,18,18119,county,Owen County,https://www.owencounty.in.gov/ +Owsley,Kentucky,189,21,21189,county,Owsley County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Owsley+County +Owyhee,Idaho,73,16,16073,county,Owyhee County,https://owyheecounty.net/ +Oxford,Maine,17,23,23017,county,Oxford County,https://www.oxfordcounty.org/ +Ozark,Missouri,153,29,29153,county,Ozark County,https://www.ozarkcounty.net/ +Ozaukee,Wisconsin,89,55,55089,county,Ozaukee County,https://www.co.ozaukee.wi.us/ +Pacific,Washington,49,53,53049,county,Pacific County,https://www.co.pacific.wa.us/ +Page,Virginia,139,51,51139,county,Page County,https://www.pagecounty.virginia.gov/ +Page,Iowa,145,19,19145,county,Page County,https://pagecounty.iowa.gov/ +Palm Beach,Florida,99,12,12099,county,Palm Beach County,https://discover.pbcgov.org/ +Palo Alto,Iowa,147,19,19147,county,Palo Alto County,https://paloaltocounty.iowa.gov/ +Palo Pinto,Texas,363,48,48363,county,Palo Pinto County,https://www.co.palo-pinto.tx.us/ +Pamlico,North Carolina,137,37,37137,county,Pamlico County,https://www.pamlicocounty.org/ +Panola,Mississippi,107,28,28107,county,Panola County,https://panolacoms.com/ +Panola,Texas,365,48,48365,county,Panola County,https://www.co.panola.tx.us/ +Park,Montana,67,30,30067,county,Park County,https://www.parkcounty.org/ +Park,Wyoming,29,56,56029,county,Park County,https://parkcounty-wy.gov/ +Park,Colorado,93,8,8093,county,Park County,https://www.parkco.us/ +Parke,Indiana,121,18,18121,county,Parke County,https://www.coveredbridges.com/ +Parker,Texas,367,48,48367,county,Parker County,https://www.parkercountytx.com/ +Parmer,Texas,369,48,48369,county,Parmer County,https://parmercounty.texas.gov/ +Pasco,Florida,101,12,12101,county,Pasco County,https://www.pascocountyfl.net/ +Pasquotank,North Carolina,139,37,37139,county,Pasquotank County,https://www.pasquotankcountync.org/ +Passaic,New Jersey,31,34,34031,county,Passaic County,https://www.passaiccountynj.org/ +Patrick,Virginia,141,51,51141,county,Patrick County,https://www.co.patrick.va.us/ +Paulding,Ohio,125,39,39125,county,Paulding County,https://www.pauldingcountyoh.com/ +Paulding,Georgia,223,13,13223,county,Paulding County,https://www.paulding.gov/ +Pawnee,Nebraska,133,31,31133,county,Pawnee County,https://co.pawnee.ne.us/ +Pawnee,Oklahoma,117,40,40117,county,Pawnee County,https://oklahoma.gov/health/locations/county-health-departments/pawnee-county-health-department.html +Pawnee,Kansas,145,20,20145,county,Pawnee County,https://www.pawneecountykansas.com/ +Payette,Idaho,75,16,16075,county,Payette County,https://www.payettecounty.org/ +Payne,Oklahoma,119,40,40119,county,Payne County,https://www.paynecounty.org/ +Peach,Georgia,225,13,13225,county,Peach County,https://www.peachcounty.net/ +Pearl River,Mississippi,109,28,28109,county,Pearl River County,https://www.pearlrivercounty.net/ +Pecos,Texas,371,48,48371,county,Pecos County,https://www.co.pecos.tx.us/ +Pembina,North Dakota,67,38,38067,county,Pembina County,https://www.pembinacountynd.gov/ +Pemiscot,Missouri,155,29,29155,county,Pemiscot County,https://www.pemiscotcounty.org/ +Pend Oreille,Washington,51,53,53051,county,Pend Oreille County,https://www.pendoreilleco.org/ +Pender,North Carolina,141,37,37141,county,Pender County,https://pendercountync.gov/ +Pendleton,Kentucky,191,21,21191,county,Pendleton County,https://pendletoncounty.ky.gov/ +Pendleton,West Virginia,71,54,54071,county,Pendleton County,https://pencowv.com/ +Pennington,Minnesota,113,27,27113,county,Pennington County,https://co.pennington.mn.us/ +Pennington,South Dakota,103,46,46103,county,Pennington County,https://www.pennco.org/ +Penobscot,Maine,19,23,23019,county,Penobscot County,https://www.penobscot-county.net/ +Peoria,Illinois,143,17,17143,county,Peoria County,https://www.peoriacounty.gov/ +Pepin,Wisconsin,91,55,55091,county,Pepin County,https://www.co.pepin.wi.us/ +Perkins,South Dakota,105,46,46105,county,Perkins County,https://www.perkinscounty.org/ +Perkins,Nebraska,135,31,31135,county,Perkins County,https://co.perkins.ne.us/ +Perquimans,North Carolina,143,37,37143,county,Perquimans County,https://www.perquimanscountync.gov/ +Perry,Missouri,157,29,29157,county,Perry County,https://perrycountymo.us/ +Perry,Arkansas,105,5,5105,county,Perry County,https://portal.arkansas.gov/counties/perr/ +Perry,Kentucky,193,21,21193,county,Perry County,https://perrycounty.ky.gov/Pages/default.aspx +Perry,Illinois,145,17,17145,county,Perry County,https://perrycountyil.gov/ +Perry,Ohio,127,39,39127,county,Perry County,http://www.perrycountyohio.net/ +Perry,Tennessee,135,47,47135,county,Perry County,https://perrycountytn.com/ +Perry,Indiana,123,18,18123,county,Perry County,https://perrycounty.in.gov/ +Perry,Alabama,105,1,1105,county,Perry County,https://www.perrycountyal.gov/ +Perry,Mississippi,111,28,28111,county,Perry County,https://www.beaumont.ms.gov/perry-county-government +Perry,Pennsylvania,99,42,42099,county,Perry County,https://perryco.org/ +Pershing,Nevada,27,32,32027,county,Pershing County,https://www.pershingcountynv.gov/ +Person,North Carolina,145,37,37145,county,Person County,https://www.personcountync.gov/ +Petersburg,Virginia,730,51,51730,city,Petersburg city,http://www.petersburg-va.org/ +Petroleum,Montana,69,30,30069,county,Petroleum County,https://petroleumcountymt.org/ +Pettis,Missouri,159,29,29159,county,Pettis County,https://pettiscomo.com/ +Phelps,Nebraska,137,31,31137,county,Phelps County,https://phelpscounty.ne.gov/ +Phelps,Missouri,161,29,29161,county,Phelps County,http://www.phelpscounty.org/ +Philadelphia,Pennsylvania,101,42,42101,county,Philadelphia County,https://www.phila.gov/ +Phillips,Arkansas,107,5,5107,county,Phillips County,https://local.arkansas.gov/local.php?agency=Phillips%20County +Phillips,Kansas,147,20,20147,county,Phillips County,https://www.phillipscountyks.org/ +Phillips,Montana,71,30,30071,county,Phillips County,https://phillco.org/local-government/ +Phillips,Colorado,95,8,8095,county,Phillips County,https://phillipscounty.colorado.gov/ +Piatt,Illinois,147,17,17147,county,Piatt County,https://piatt.gov/ +Pickaway,Ohio,129,39,39129,county,Pickaway County,https://pickaway.org/ +Pickens,South Carolina,77,45,45077,county,Pickens County,https://www.co.pickens.sc.us/ +Pickens,Alabama,107,1,1107,county,Pickens County,https://www.pickenscountyal.com/ +Pickens,Georgia,227,13,13227,county,Pickens County,https://pickensga.com/ +Pickett,Tennessee,137,47,47137,county,Pickett County,https://dalehollow.com/pickett-county-government +Pierce,Georgia,229,13,13229,county,Pierce County,https://piercecountyga.gov/ +Pierce,North Dakota,69,38,38069,county,Pierce County,https://www.piercecountynd.gov/ +Pierce,Wisconsin,93,55,55093,county,Pierce County,https://www.co.pierce.wi.us/ +Pierce,Nebraska,139,31,31139,county,Pierce County,https://co.pierce.ne.us/ +Pierce,Washington,53,53,53053,county,Pierce County,https://www.piercecountywa.gov/ +Pike,Missouri,163,29,29163,county,Pike County,https://pikecountymo.net/ +Pike,Illinois,149,17,17149,county,Pike County,https://www.pikecountyil.org/ +Pike,Mississippi,113,28,28113,county,Pike County,https://www.co.pike.ms.us/ +Pike,Kentucky,195,21,21195,county,Pike County,http://www.pikecountyky.gov/ +Pike,Ohio,131,39,39131,county,Pike County,https://www.pikecountycourt.org/ +Pike,Alabama,109,1,1109,county,Pike County,https://pikeprobate.com/ +Pike,Georgia,231,13,13231,county,Pike County,https://www.pikecoga.com/ +Pike,Arkansas,109,5,5109,county,Pike County,https://pikecountyar.org/ +Pike,Indiana,125,18,18125,county,Pike County,https://www.pikecounty.in.gov/ +Pike,Pennsylvania,103,42,42103,county,Pike County,https://www.pikepa.org/ +Pima,Arizona,19,4,4019,county,Pima County,https://www.pima.gov/ +Pinal,Arizona,21,4,4021,county,Pinal County,https://www.pinal.gov/ +Pine,Minnesota,115,27,27115,county,Pine County,https://www.co.pine.mn.us/ +Pinellas,Florida,103,12,12103,county,Pinellas County,https://pinellas.gov/ +Pipestone,Minnesota,117,27,27117,county,Pipestone County,https://www.pipestone-county.com/ +Piscataquis,Maine,21,23,23021,county,Piscataquis County,https://www.piscataquis.us/ +Pitkin,Colorado,97,8,8097,county,Pitkin County,https://pitkincounty.com/ +Pitt,North Carolina,147,37,37147,county,Pitt County,https://www.pittcountync.gov/ +Pittsburg,Oklahoma,121,40,40121,county,Pittsburg County,https://pittsburg.okcounties.org/ +Pittsylvania,Virginia,143,51,51143,county,Pittsylvania County,https://www.pittsylvaniacountyva.gov/ +Piute,Utah,31,49,49031,county,Piute County,https://www.piuteutah.com/ +Placer,California,61,6,6061,county,Placer County,https://www.placer.ca.gov/ +Plaquemines,Louisiana,75,22,22075,parish,Plaquemines Parish,https://plaqueminesparish.com/ +Platte,Nebraska,141,31,31141,county,Platte County,https://plattecounty.net/ +Platte,Missouri,165,29,29165,county,Platte County,https://www.co.platte.mo.us/ +Platte,Wyoming,31,56,56031,county,Platte County,https://www.plattecountywyoming.com/ +Pleasants,West Virginia,73,54,54073,county,Pleasants County,https://www.wv.gov/local/Pages/counties.aspx?county=pleasants +Plumas,California,63,6,6063,county,Plumas County,https://www.plumascounty.us/ +Plymouth,Massachusetts,23,25,25023,county,Plymouth County,https://www.plymouthcountyma.gov/ +Plymouth,Iowa,149,19,19149,county,Plymouth County,https://www.co.plymouth.ia.us/ +Pocahontas,Iowa,151,19,19151,county,Pocahontas County,https://pocahontascounty.iowa.gov/ +Pocahontas,West Virginia,75,54,54075,county,Pocahontas County,https://pocahontascountywv.com/ +Poinsett,Arkansas,111,5,5111,county,Poinsett County,https://www.poinsettcounty.us/ +Pointe Coupee,Louisiana,77,22,22077,parish,Pointe Coupee Parish,https://pcparish.org/ +Polk,Iowa,153,19,19153,county,Polk County,https://www.polkcountyiowa.gov/ +Polk,Texas,373,48,48373,county,Polk County,https://www.co.polk.tx.us/ +Polk,Missouri,167,29,29167,county,Polk County,https://polkcountymo.gov/ +Polk,Georgia,233,13,13233,county,Polk County,https://www.polkga.org/ +Polk,North Carolina,149,37,37149,county,Polk County,https://www.polknc.gov/ +Polk,Minnesota,119,27,27119,county,Polk County,https://www.co.polk.mn.us/ +Polk,Oregon,53,41,41053,county,Polk County,https://www.co.polk.or.us/home +Polk,Tennessee,139,47,47139,county,Polk County,https://www.polkgovernment.com/ +Polk,Wisconsin,95,55,55095,county,Polk County,https://www.polkcountywi.gov/ +Polk,Nebraska,143,31,31143,county,Polk County,https://polkcounty.nebraska.gov/ +Polk,Florida,105,12,12105,county,Polk County,https://www.polk-county.net/ +Polk,Arkansas,113,5,5113,county,Polk County,https://portal.arkansas.gov/counties/polk/ +Pondera,Montana,73,30,30073,county,Pondera County,https://www.ponderacountymontana.org/ +Pontotoc,Mississippi,115,28,28115,county,Pontotoc County,https://www.mssupervisors.org/ms-counties/pontotoc +Pontotoc,Oklahoma,123,40,40123,county,Pontotoc County,https://pontotoc.okcounties.org/ +Pope,Illinois,151,17,17151,county,Pope County,https://www.ilsos.gov/departments/archives/IRAD/pope.html +Pope,Arkansas,115,5,5115,county,Pope County,https://www.popecountyar.gov/ +Pope,Minnesota,121,27,27121,county,Pope County,https://www.popecountymn.gov/ +Poquoson,Virginia,735,51,51735,city,Poquoson city,https://www.ci.poquoson.va.us/ +Portage,Wisconsin,97,55,55097,county,Portage County,https://www.co.portage.wi.gov/ +Portage,Ohio,133,39,39133,county,Portage County,https://www.portagecounty-oh.gov/ +Porter,Indiana,127,18,18127,county,Porter County,https://www.porterco.org/ +Portsmouth,Virginia,740,51,51740,city,Portsmouth city,https://www.portsmouthva.gov/ +Posey,Indiana,129,18,18129,county,Posey County,https://www.poseycountyin.gov/ +Pottawatomie,Oklahoma,125,40,40125,county,Pottawatomie County,https://www.pottawatomiecountyok.gov/ +Pottawatomie,Kansas,149,20,20149,county,Pottawatomie County,https://www.pottcounty.org/ +Pottawattamie,Iowa,155,19,19155,county,Pottawattamie County,https://www.pottcounty-ia.gov/ +Potter,South Dakota,107,46,46107,county,Potter County,"https://en.wikipedia.org/wiki/Potter_County,_South_Dakota" +Potter,Texas,375,48,48375,county,Potter County,https://www.co.potter.tx.us/ +Potter,Pennsylvania,105,42,42105,county,Potter County,https://pottercountypa.net/ +Powder River,Montana,75,30,30075,county,Powder River County,https://prco.mt.gov/ +Powell,Montana,77,30,30077,county,Powell County,https://www.powellcountymt.gov/ +Powell,Kentucky,197,21,21197,county,Powell County,http://www.powellcountyky.us/ +Power,Idaho,77,16,16077,county,Power County,https://www.co.power.id.us/ +Poweshiek,Iowa,157,19,19157,county,Poweshiek County,https://poweshiekcounty.org/ +Powhatan,Virginia,145,51,51145,county,Powhatan County,https://www.powhatanva.gov/ +Prairie,Montana,79,30,30079,county,Prairie County,https://prairiecounty.org/ +Prairie,Arkansas,117,5,5117,county,Prairie County,https://local.arkansas.gov/local.php?agency=Prairie%20County +Pratt,Kansas,151,20,20151,county,Pratt County,https://www.prattcounty.org/ +Preble,Ohio,135,39,39135,county,Preble County,https://www.prebco.org/ +Prentiss,Mississippi,117,28,28117,county,Prentiss County,http://www.prentisscounty.org/ +Presidio,Texas,377,48,48377,county,Presidio County,https://www.co.presidio.tx.us/ +Presque Isle,Michigan,141,26,26141,county,Presque Isle County,https://presqueislecounty.org/ +Preston,West Virginia,77,54,54077,county,Preston County,https://prestoncountywv.gov/ +Price,Wisconsin,99,55,55099,county,Price County,https://www.co.price.wi.us/ +Prince Edward,Virginia,147,51,51147,county,Prince Edward County,https://www.co.prince-edward.va.us/ +Prince George,Virginia,149,51,51149,county,Prince George County,https://www.princegeorgecountyva.gov/ +Prince George's,Maryland,33,24,24033,county,Prince George's County,https://www.princegeorgescountymd.gov/government +Prince William,Virginia,153,51,51153,county,Prince William County,https://www.pwcva.gov/ +Providence,Rhode Island,7,44,44007,county,Providence County,https://www.providenceri.gov/ +Prowers,Colorado,99,8,8099,county,Prowers County,https://www.prowerscounty.net/ +Pueblo,Colorado,101,8,8101,county,Pueblo County,https://county.pueblo.org/ +Pulaski,Virginia,155,51,51155,county,Pulaski County,https://www.pulaskicounty.org/ +Pulaski,Illinois,153,17,17153,county,Pulaski County,https://www.pulaskicountyil.net/index.html +Pulaski,Arkansas,119,5,5119,county,Pulaski County,https://www.pulaskicounty.net/ +Pulaski,Georgia,235,13,13235,county,Pulaski County,https://hawkinsville-pulaski.org/ +Pulaski,Indiana,131,18,18131,county,Pulaski County,http://pulaskionline.org/ +Pulaski,Missouri,169,29,29169,county,Pulaski County,http://www.pulaskicountymo.org/home.html +Pulaski,Kentucky,199,21,21199,county,Pulaski County,https://www.pulaskigov.com/ +Pushmataha,Oklahoma,127,40,40127,county,Pushmataha County,https://oklahoma.gov/okdhs/library/resources/pushmatahard211.html +Putnam,New York,79,36,36079,county,Putnam County,https://www.putnamcountyny.com/ +Putnam,Florida,107,12,12107,county,Putnam County,https://main.putnam-fl.com/ +Putnam,West Virginia,79,54,54079,county,Putnam County,https://putnamcountygov.com/ +Putnam,Tennessee,141,47,47141,county,Putnam County,https://putnamcountytn.gov/ +Putnam,Illinois,155,17,17155,county,Putnam County,https://putnamil.gov/ +Putnam,Indiana,133,18,18133,county,Putnam County,https://co.putnam.in.us/ +Putnam,Ohio,137,39,39137,county,Putnam County,https://putnamcountyohio.gov/ +Putnam,Missouri,171,29,29171,county,Putnam County,http://www.unionvillemo.org/government/putnam_county/index.php +Putnam,Georgia,237,13,13237,county,Putnam County,https://www.putnamcountyga.us/home +Quay,New Mexico,37,35,35037,county,Quay County,https://www.quaycounty-nm.gov/ +Queen Anne's,Maryland,35,24,24035,county,Queen Anne's County,https://www.qac.org/ +Queens,New York,81,36,36081,county,Queens County,https://www.ny.gov/counties/queens +Quitman,Georgia,239,13,13239,county,Quitman County,https://www.gqc-ga.org/ +Quitman,Mississippi,119,28,28119,county,Quitman County,https://quitmancountyms.org/ +Rabun,Georgia,241,13,13241,county,Rabun County,https://rabuncounty.ga.gov/home +Racine,Wisconsin,101,55,55101,county,Racine County,https://www.racinecounty.com/ +Radford,Virginia,750,51,51750,city,Radford city,https://www.radfordva.gov/ +Rains,Texas,379,48,48379,county,Rains County,https://www.co.rains.tx.us/ +Raleigh,West Virginia,81,54,54081,county,Raleigh County,https://raleighcounty.org/ +Ralls,Missouri,173,29,29173,county,Ralls County,http://www.rallscounty.org/ +Ramsey,Minnesota,123,27,27123,county,Ramsey County,https://www.ramseycounty.us/home +Ramsey,North Dakota,71,38,38071,county,Ramsey County,https://www.ramseycountynd.gov/ +Randall,Texas,381,48,48381,county,Randall County,https://www.randallcounty.gov/ +Randolph,Georgia,243,13,13243,county,Randolph County,https://www.randolphcountyga.com/ +Randolph,Indiana,135,18,18135,county,Randolph County,https://www.in.gov/counties/randolph/ +Randolph,Missouri,175,29,29175,county,Randolph County,https://www.randolphcounty-mo.com/ +Randolph,Arkansas,121,5,5121,county,Randolph County,https://local.arkansas.gov/local.php?agency=randolph%20county +Randolph,Alabama,111,1,1111,county,Randolph County,http://www.randolphcountyalabama.gov/ +Randolph,West Virginia,83,54,54083,county,Randolph County,https://randolphcountycommissionwv.org/ +Randolph,Illinois,157,17,17157,county,Randolph County,https://am.randolphco.org/ +Randolph,North Carolina,151,37,37151,county,Randolph County,https://www.randolphcountync.gov/ +Rankin,Mississippi,121,28,28121,county,Rankin County,https://www.rankincounty.org/ +Ransom,North Dakota,73,38,38073,county,Ransom County,https://ransomcountynd.net/ +Rapides,Louisiana,79,22,22079,parish,Rapides Parish,https://www.louisiana.gov/local-louisiana/rapides-parish +Rappahannock,Virginia,157,51,51157,county,Rappahannock County,https://www.rappahannockcountyva.gov/ +Ravalli,Montana,81,30,30081,county,Ravalli County,https://ravalli.us/ +Rawlins,Kansas,153,20,20153,county,Rawlins County,https://sites.google.com/a/rawlinscounty.org/rawlins-county-courthouse/ +Ray,Missouri,177,29,29177,county,Ray County,https://raycountymo.com/ +Reagan,Texas,383,48,48383,county,Reagan County,https://www.co.reagan.tx.us/ +Real,Texas,385,48,48385,county,Real County,https://www.co.real.tx.us/ +Red Lake,Minnesota,125,27,27125,county,Red Lake County,https://www.co.red-lake.mn.us/ +Red River,Louisiana,81,22,22081,parish,Red River Parish,https://www.louisiana.gov/local-louisiana/red-river-parish +Red River,Texas,387,48,48387,county,Red River County,https://www.co.red-river.tx.us/ +Red Willow,Nebraska,145,31,31145,county,Red Willow County,https://co.red-willow.ne.us/ +Redwood,Minnesota,127,27,27127,county,Redwood County,https://redwoodcounty-mn.us/ +Reeves,Texas,389,48,48389,county,Reeves County,https://www.reevescounty.org/ +Refugio,Texas,391,48,48391,county,Refugio County,https://www.co.refugio.tx.us/ +Reno,Kansas,155,20,20155,county,Reno County,https://www.renogov.org/ +Rensselaer,New York,83,36,36083,county,Rensselaer County,https://www.rensco.com/ +Renville,Minnesota,129,27,27129,county,Renville County,https://www.renvillecountymn.gov/ +Renville,North Dakota,75,38,38075,county,Renville County,https://www.renvillecountynd.org/ +Republic,Kansas,157,20,20157,county,Republic County,http://www.republiccounty.org/ +Reynolds,Missouri,179,29,29179,county,Reynolds County,https://www.mocounties.com/reynolds-county +Rhea,Tennessee,143,47,47143,county,Rhea County,http://rheacountytn.com/ +Rice,Kansas,159,20,20159,county,Rice County,https://www.ricecounty.us/ +Rice,Minnesota,131,27,27131,county,Rice County,https://www.ricecountymn.gov/ +Rich,Utah,33,49,49033,county,Rich County,https://www.richcountyut.org/ +Richardson,Nebraska,147,31,31147,county,Richardson County,https://co.richardson.ne.us/ +Richland,Ohio,139,39,39139,county,Richland County,https://www.richlandcountyoh.gov/ +Richland,Louisiana,83,22,22083,parish,Richland Parish,https://www.louisiana.gov/local-louisiana/richland-parish +Richland,North Dakota,77,38,38077,county,Richland County,https://www.co.richland.nd.us/ +Richland,Montana,83,30,30083,county,Richland County,https://www.richland.org/ +Richland,Illinois,159,17,17159,county,Richland County,https://www.ilsos.gov/departments/archives/IRAD/richland.html +Richland,Wisconsin,103,55,55103,county,Richland County,https://co.richland.wi.us/ +Richland,South Carolina,79,45,45079,county,Richland County,https://www.richlandcountysc.gov/ +Richmond City,Virginia,760,51,51760,city,Richmond city,https://www.rva.gov/ +Richmond,Virginia,159,51,51159,county,Richmond County,https://co.richmond.va.us/ +Richmond,New York,85,36,36085,county,Richmond County,https://www.ny.gov/counties/richmond +Richmond,Georgia,245,13,13245,county,Richmond County,https://www.augustaga.gov/ +Richmond,North Carolina,153,37,37153,county,Richmond County,https://www.richmondnc.com/ +Riley,Kansas,161,20,20161,county,Riley County,https://www.rileycountyks.gov/ +Ringgold,Iowa,159,19,19159,county,Ringgold County,https://www.ringgoldcounty.iowa.gov/ +Rio Arriba,New Mexico,39,35,35039,county,Rio Arriba County,https://www.rio-arriba.org/ +Rio Blanco,Colorado,103,8,8103,county,Rio Blanco County,https://www.rbc.us/ +Rio Grande,Colorado,105,8,8105,county,Rio Grande County,https://www.riograndecounty.org/ +Ripley,Missouri,181,29,29181,county,Ripley County,https://www.ripleycountymissouri.org/government.php +Ripley,Indiana,137,18,18137,county,Ripley County,https://www.ripleycounty.in.gov/ +Ritchie,West Virginia,85,54,54085,county,Ritchie County,https://ritchiecounty.wv.gov/ +Riverside,California,65,6,6065,county,Riverside County,https://rivco.org/ +Roane,Tennessee,145,47,47145,county,Roane County,https://roanecountytn.gov/ +Roane,West Virginia,87,54,54087,county,Roane County,https://www.roanewv.com/ +Roanoke City,Virginia,770,51,51770,city,Roanoke city,https://www.roanokeva.gov/ +Roanoke,Virginia,161,51,51161,county,Roanoke County,https://www.roanokecountyva.gov/ +Roberts,Texas,393,48,48393,county,Roberts County,https://www.co.roberts.tx.us/ +Roberts,South Dakota,109,46,46109,county,Roberts County,https://roberts.sdcounties.org/ +Robertson,Texas,395,48,48395,county,Robertson County,https://www.co.robertson.tx.us/ +Robertson,Kentucky,201,21,21201,county,Robertson County,https://robertsoncounty.ky.gov/ +Robertson,Tennessee,147,47,47147,county,Robertson County,https://www.robertsoncountytn.gov/ +Robeson,North Carolina,155,37,37155,county,Robeson County,https://www.robesoncountync.gov/ +Rock,Minnesota,133,27,27133,county,Rock County,https://www.co.rock.mn.us/ +Rock,Nebraska,149,31,31149,county,Rock County,https://rockcountyne.gov/ +Rock,Wisconsin,105,55,55105,county,Rock County,https://www.co.rock.wi.us/ +Rock Island,Illinois,161,17,17161,county,Rock Island County,https://www.rockislandcountyil.gov/ +Rockbridge,Virginia,163,51,51163,county,Rockbridge County,https://www.co.rockbridge.va.us/ +Rockcastle,Kentucky,203,21,21203,county,Rockcastle County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Rockcastle+County +Rockdale,Georgia,247,13,13247,county,Rockdale County,https://www.rockdalecountyga.gov/ +Rockingham,Virginia,165,51,51165,county,Rockingham County,https://www.rockinghamcountyva.gov/ +Rockingham,New Hampshire,15,33,33015,county,Rockingham County,https://rockinghamcountynh.org/ +Rockingham,North Carolina,157,37,37157,county,Rockingham County,https://www.rockinghamcountync.gov/ +Rockland,New York,87,36,36087,county,Rockland County,https://rocklandgov.com/ +Rockwall,Texas,397,48,48397,county,Rockwall County,https://www.rockwallcountytexas.com/ +Roger Mills,Oklahoma,129,40,40129,county,Roger Mills County,https://oklahoma.gov/okdhs/library/resources/rogermillsrd211.html +Rogers,Oklahoma,131,40,40131,county,Rogers County,https://www.rogerscounty.org/ +Rolette,North Dakota,79,38,38079,county,Rolette County,https://www.rolettecounty.com/ +Rooks,Kansas,163,20,20163,county,Rooks County,https://rookscounty.net/ +Roosevelt,Montana,85,30,30085,county,Roosevelt County,https://www.rooseveltcountymt.gov/ +Roosevelt,New Mexico,41,35,35041,county,Roosevelt County,https://www.rooseveltcounty.com/ +Roscommon,Michigan,143,26,26143,county,Roscommon County,https://www.roscommoncounty.net/ +Roseau,Minnesota,135,27,27135,county,Roseau County,https://www.co.roseau.mn.us/ +Rosebud,Montana,87,30,30087,county,Rosebud County,https://rosebudcountymt.gov/ +Ross,Ohio,141,39,39141,county,Ross County,http://www.co.ross.oh.us/ +Routt,Colorado,107,8,8107,county,Routt County,https://www.co.routt.co.us/ +Rowan,North Carolina,159,37,37159,county,Rowan County,https://www.rowancountync.gov/ +Rowan,Kentucky,205,21,21205,county,Rowan County,https://www.rcky.us/ +Runnels,Texas,399,48,48399,county,Runnels County,https://www.runnelscounty.org/ +Rush,Kansas,165,20,20165,county,Rush County,https://rushcountykansas.org/ +Rush,Indiana,139,18,18139,county,Rush County,https://rushcounty.in.gov/ +Rusk,Wisconsin,107,55,55107,county,Rusk County,https://ruskcounty.org/ +Rusk,Texas,401,48,48401,county,Rusk County,https://www.ruskcountytx.gov/ +Russell,Alabama,113,1,1113,county,Russell County,https://rcala.com/ +Russell,Virginia,167,51,51167,county,Russell County,https://www.russellcountyva.us/ +Russell,Kansas,167,20,20167,county,Russell County,https://www.russellcountykansas.com/ +Russell,Kentucky,207,21,21207,county,Russell County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Russell+County +Rutherford,Tennessee,149,47,47149,county,Rutherford County,https://rutherfordcountytn.gov/ +Rutherford,North Carolina,161,37,37161,county,Rutherford County,https://www.rutherfordcountync.gov/ +Rutland,Vermont,21,50,50021,county,Rutland County,http://www.vermont.gov/ +Sabine,Texas,403,48,48403,county,Sabine County,http://www.co.sabine.tx.us/ +Sabine,Louisiana,85,22,22085,parish,Sabine Parish,https://www.louisiana.gov/local-louisiana/sabine-parish +Sac,Iowa,161,19,19161,county,Sac County,https://www.saccountyiowa.gov/ +Sacramento,California,67,6,6067,county,Sacramento County,https://www.saccounty.net/ +Sagadahoc,Maine,23,23,23023,county,Sagadahoc County,https://www.sagadahoccountyme.gov/ +Saginaw,Michigan,145,26,26145,county,Saginaw County,https://www.saginawcounty.com/ +Saguache,Colorado,109,8,8109,county,Saguache County,https://saguachecounty.colorado.gov/ +Salem,New Jersey,33,34,34033,county,Salem County,https://www.salemcountynj.gov/ +Salem,Virginia,775,51,51775,city,Salem city,https://salemva.gov/ +Saline,Kansas,169,20,20169,county,Saline County,https://www.salinecountyks.gov/ +Saline,Arkansas,125,5,5125,county,Saline County,https://www.salinecounty.org/ +Saline,Illinois,165,17,17165,county,Saline County,https://www.salinecounty.illinois.gov/ +Saline,Missouri,195,29,29195,county,Saline County,https://www.salinecountymo.org/ +Saline,Nebraska,151,31,31151,county,Saline County,https://co.saline.ne.us/ +Salt Lake,Utah,35,49,49035,county,Salt Lake County,https://slco.org/ +Saluda,South Carolina,81,45,45081,county,Saluda County,https://saludacounty.sc.gov/ +Sampson,North Carolina,163,37,37163,county,Sampson County,https://www.sampsonnc.com/ +San Augustine,Texas,405,48,48405,county,San Augustine County,https://www.co.san-augustine.tx.us/ +San Benito,California,69,6,6069,county,San Benito County,https://www.cosb.us/ +San Bernardino,California,71,6,6071,county,San Bernardino County,https://main.sbcounty.gov/ +San Diego,California,73,6,6073,county,San Diego County,https://www.sandiegocounty.gov/ +San Francisco,California,75,6,6075,county,San Francisco County,https://sf.gov/ +San Jacinto,Texas,407,48,48407,county,San Jacinto County,https://www.co.san-jacinto.tx.us/ +San Joaquin,California,77,6,6077,county,San Joaquin County,http://www.sjgov.org/ +San Juan,Colorado,111,8,8111,county,San Juan County,https://sanjuancounty.colorado.gov/ +San Juan,Washington,55,53,53055,county,San Juan County,https://www.sanjuanco.com/ +San Juan,New Mexico,45,35,35045,county,San Juan County,https://www.sjcounty.net/ +San Juan,Utah,37,49,49037,county,San Juan County,https://sanjuancounty.org/home +San Luis Obispo,California,79,6,6079,county,San Luis Obispo County,https://www.slocounty.ca.gov/ +San Mateo,California,81,6,6081,county,San Mateo County,https://www.smcgov.org/ +San Miguel,New Mexico,47,35,35047,county,San Miguel County,https://co.sanmiguel.nm.us/ +San Miguel,Colorado,113,8,8113,county,San Miguel County,https://www.sanmiguelcountyco.gov/ +San Patricio,Texas,409,48,48409,county,San Patricio County,https://www.co.san-patricio.tx.us/ +San Saba,Texas,411,48,48411,county,San Saba County,https://www.co.san-saba.tx.us/ +Sanborn,South Dakota,111,46,46111,county,Sanborn County,https://dot.sd.gov/sanborncounty-pcn-06pd +Sanders,Montana,89,30,30089,county,Sanders County,https://co.sanders.mt.us/ +Sandoval,New Mexico,43,35,35043,county,Sandoval County,https://www.sandovalcountynm.gov/ +Sandusky,Ohio,143,39,39143,county,Sandusky County,https://sanduskycountyoh.gov/ +Sangamon,Illinois,167,17,17167,county,Sangamon County,https://co.sangamon.il.us/ +Sanilac,Michigan,151,26,26151,county,Sanilac County,https://www.sanilaccounty.net/ +Sanpete,Utah,39,49,49039,county,Sanpete County,https://www.sanpete.com/ +Santa Barbara,California,83,6,6083,county,Santa Barbara County,https://www.countyofsb.org/ +Santa Clara,California,85,6,6085,county,Santa Clara County,https://www.santaclaracounty.gov/home +Santa Cruz,Arizona,23,4,4023,county,Santa Cruz County,https://www.santacruzcountyaz.gov/ +Santa Cruz,California,87,6,6087,county,Santa Cruz County,https://www.santacruzcountyca.gov/ +Santa Fe,New Mexico,49,35,35049,county,Santa Fe County,https://www.santafecountynm.gov/ +Santa Rosa,Florida,113,12,12113,county,Santa Rosa County,https://www.santarosa.fl.gov/ +Sarasota,Florida,115,12,12115,county,Sarasota County,https://www.scgov.net/ +Saratoga,New York,91,36,36091,county,Saratoga County,https://www.saratogacountyny.gov/ +Sargent,North Dakota,81,38,38081,county,Sargent County,http://www.sargentnd.com/ +Sarpy,Nebraska,153,31,31153,county,Sarpy County,https://www.sarpy.gov/ +Sauk,Wisconsin,111,55,55111,county,Sauk County,https://www.co.sauk.wi.us/home +Saunders,Nebraska,155,31,31155,county,Saunders County,https://saunderscounty.ne.gov/ +Sawyer,Wisconsin,113,55,55113,county,Sawyer County,https://www.sawyercountygov.org/ +Schenectady,New York,93,36,36093,county,Schenectady County,https://www.schenectadycountyny.gov/home +Schleicher,Texas,413,48,48413,county,Schleicher County,https://www.co.schleicher.tx.us/ +Schley,Georgia,249,13,13249,county,Schley County,https://schleycountyga.us/ +Schoharie,New York,95,36,36095,county,Schoharie County,https://www4.schohariecounty-ny.gov/ +Schoolcraft,Michigan,153,26,26153,county,Schoolcraft County,https://www.schoolcraftcounty.net/ +Schuyler,Illinois,169,17,17169,county,Schuyler County,https://www.schuylercounty.org/ +Schuyler,New York,97,36,36097,county,Schuyler County,https://www.schuylercounty.us/ +Schuyler,Missouri,197,29,29197,county,Schuyler County,https://www.schuylercountymo.org/ +Schuylkill,Pennsylvania,107,42,42107,county,Schuylkill County,https://schuylkillcountypa.gov/ +Scioto,Ohio,145,39,39145,county,Scioto County,https://www.sciotocountyoh.com/ +Scotland,Missouri,199,29,29199,county,Scotland County,https://www.scotlandcountymo.org/ +Scotland,North Carolina,165,37,37165,county,Scotland County,https://www.scotlandcounty.org/ +Scott,Mississippi,123,28,28123,county,Scott County,https://www.scottcountyms.gov/ +Scott,Minnesota,139,27,27139,county,Scott County,https://www.scottcountymn.gov/ +Scott,Illinois,171,17,17171,county,Scott County,https://www.ilsos.gov/departments/archives/IRAD/scott.html +Scott,Tennessee,151,47,47151,county,Scott County,https://scottcounty.com/ +Scott,Missouri,201,29,29201,county,Scott County,https://www.scottcountymo.com/ +Scott,Kansas,171,20,20171,county,Scott County,https://scottcountyks.com/ +Scott,Kentucky,209,21,21209,county,Scott County,https://scottky.gov/ +Scott,Indiana,143,18,18143,county,Scott County,https://www.scottcounty.in.gov/ +Scott,Arkansas,127,5,5127,county,Scott County,https://scottcountyar.com/ +Scott,Virginia,169,51,51169,county,Scott County,https://www.scottcountyva.com/ +Scott,Iowa,163,19,19163,county,Scott County,https://www.scottcountyiowa.gov/ +Scotts Bluff,Nebraska,157,31,31157,county,Scotts Bluff County,https://www.scottsbluffcounty.org/ +Screven,Georgia,251,13,13251,county,Screven County,https://www.screvencountyboc.com/ +Scurry,Texas,415,48,48415,county,Scurry County,https://www.co.scurry.tx.us/ +Searcy,Arkansas,129,5,5129,county,Searcy County,https://local.arkansas.gov/local.php?agency=searcy%20county +Sebastian,Arkansas,131,5,5131,county,Sebastian County,https://www.sebastiancountyar.gov/ +Sedgwick,Kansas,173,20,20173,county,Sedgwick County,https://www.sedgwickcounty.org/ +Sedgwick,Colorado,115,8,8115,county,Sedgwick County,https://sedgwickcounty.colorado.gov/ +Seminole,Georgia,253,13,13253,county,Seminole County,https://www.donalsonville-seminole.org/your-government/ +Seminole,Florida,117,12,12117,county,Seminole County,https://www.seminolecountyfl.gov/ +Seminole,Oklahoma,133,40,40133,county,Seminole County,https://seminolecountyok.com/ +Seneca,Ohio,147,39,39147,county,Seneca County,https://senecacountyohio.gov/ +Seneca,New York,99,36,36099,county,Seneca County,https://www.co.seneca.ny.us/ +Sequatchie,Tennessee,153,47,47153,county,Sequatchie County,https://sequatchiecountytn.gov/ +Sequoyah,Oklahoma,135,40,40135,county,Sequoyah County,https://oklahoma.gov/health/locations/county-health-departments/sequoyah-county-health-department.html +Sevier,Tennessee,155,47,47155,county,Sevier County,https://www.seviercountytn.gov/ +Sevier,Utah,41,49,49041,county,Sevier County,https://www.sevierutah.net/ +Sevier,Arkansas,133,5,5133,county,Sevier County,https://www.seviercountyar.org/ +Seward,Nebraska,159,31,31159,county,Seward County,https://www.sewardcountyne.gov/ +Seward,Kansas,175,20,20175,county,Seward County,https://www.sewardcountyks.org/ +Shackelford,Texas,417,48,48417,county,Shackelford County,https://www.shackelfordcounty.org/ +Shannon,Missouri,203,29,29203,county,Shannon County,https://www.shannon-county.com/ +Sharkey,Mississippi,125,28,28125,county,Sharkey County,http://www.sharkeycounty.net/ +Sharp,Arkansas,135,5,5135,county,Sharp County,https://local.arkansas.gov/local.php?agency=Sharp%20County +Shasta,California,89,6,6089,county,Shasta County,https://www.shastacounty.gov/ +Shawano,Wisconsin,115,55,55115,county,Shawano County,https://www.co.shawano.wi.us/ +Shawnee,Kansas,177,20,20177,county,Shawnee County,https://www.snco.us/ +Sheboygan,Wisconsin,117,55,55117,county,Sheboygan County,https://www.sheboygancounty.com/ +Shelby,Iowa,165,19,19165,county,Shelby County,https://shelbycounty.iowa.gov/ +Shelby,Missouri,205,29,29205,county,Shelby County,https://shelbycountymo.com/ +Shelby,Kentucky,211,21,21211,county,Shelby County,https://shelbycounty.ky.gov/Pages/index.aspx +Shelby,Tennessee,157,47,47157,county,Shelby County,https://www.shelbycountytn.gov/ +Shelby,Ohio,149,39,39149,county,Shelby County,https://co.shelby.oh.us/ +Shelby,Indiana,145,18,18145,county,Shelby County,https://www.co.shelby.in.us/ +Shelby,Texas,419,48,48419,county,Shelby County,https://www.co.shelby.tx.us/ +Shelby,Alabama,117,1,1117,county,Shelby County,https://www.shelbyal.com/ +Shelby,Illinois,173,17,17173,county,Shelby County,https://www.shelbycounty-il.com/ +Shenandoah,Virginia,171,51,51171,county,Shenandoah County,https://shenandoahcountyva.us/ +Sherburne,Minnesota,141,27,27141,county,Sherburne County,https://www.co.sherburne.mn.us/ +Sheridan,Nebraska,161,31,31161,county,Sheridan County,https://sheridancounty.ne.gov/ +Sheridan,North Dakota,83,38,38083,county,Sheridan County,http://www.co.sheridan.nd.us/ +Sheridan,Wyoming,33,56,56033,county,Sheridan County,https://www.sheridancountywy.gov/ +Sheridan,Montana,91,30,30091,county,Sheridan County,https://www.sheridancountymt.gov/ +Sheridan,Kansas,179,20,20179,county,Sheridan County,https://www.sheridancountyks.gov/ +Sherman,Oregon,55,41,41055,county,Sherman County,https://www.co.sherman.or.us/ +Sherman,Kansas,181,20,20181,county,Sherman County,https://shermancountyks.gov/ +Sherman,Texas,421,48,48421,county,Sherman County,https://www.co.sherman.tx.us/ +Sherman,Nebraska,163,31,31163,county,Sherman County,https://shermancounty.nebraska.gov/ +Shiawassee,Michigan,155,26,26155,county,Shiawassee County,https://shiawassee.net/ +Shoshone,Idaho,79,16,16079,county,Shoshone County,https://shoshonecounty.id.gov/ +Sibley,Minnesota,143,27,27143,county,Sibley County,https://www.sibleycounty.gov/ +Sierra,California,91,6,6091,county,Sierra County,https://www.sierracounty.ca.gov/ +Sierra,New Mexico,51,35,35051,county,Sierra County,https://www.sierraco.org/ +Silver Bow,Montana,93,30,30093,county,Silver Bow County,https://co.silverbow.mt.us/ +Simpson,Mississippi,127,28,28127,county,Simpson County,http://simpsoncountyms.com/ +Simpson,Kentucky,213,21,21213,county,Simpson County,https://simpsoncountyky.gov/ +Sioux,North Dakota,85,38,38085,county,Sioux County,https://www.ndcourts.gov/court-locations/sioux-county +Sioux,Nebraska,165,31,31165,county,Sioux County,https://co.sioux.ne.us/ +Sioux,Iowa,167,19,19167,county,Sioux County,https://siouxcountyia.gov/ +Siskiyou,California,93,6,6093,county,Siskiyou County,https://www.co.siskiyou.ca.us/ +Skagit,Washington,57,53,53057,county,Skagit County,https://www.skagitcounty.net/ +Skamania,Washington,59,53,53059,county,Skamania County,https://www.skamaniacounty.org/ +Slope,North Dakota,87,38,38087,county,Slope County,https://www.slopecountynd.gov/ +Smith,Kansas,183,20,20183,county,Smith County,https://ks1495.cichosting.com/ +Smith,Tennessee,159,47,47159,county,Smith County,https://smithcotn.com/ +Smith,Mississippi,129,28,28129,county,Smith County,http://smithcountyms.org/ +Smith,Texas,423,48,48423,county,Smith County,https://www.smith-county.com/ +Smyth,Virginia,173,51,51173,county,Smyth County,http://www.smythcounty.org/ +Snohomish,Washington,61,53,53061,county,Snohomish County,https://snohomishcountywa.gov/ +Snyder,Pennsylvania,109,42,42109,county,Snyder County,https://www.snydercounty.org/ +Socorro,New Mexico,53,35,35053,county,Socorro County,https://www.socorrocounty.net/ +Solano,California,95,6,6095,county,Solano County,https://www.solanocounty.com/ +Somerset,Pennsylvania,111,42,42111,county,Somerset County,http://www.co.somerset.pa.us/ +Somerset,Maine,25,23,23025,county,Somerset County,https://www.somersetcounty-me.org/ +Somerset,New Jersey,35,34,34035,county,Somerset County,https://www.co.somerset.nj.us/ +Somerset,Maryland,39,24,24039,county,Somerset County,https://www.somersetmd.us/ +Somervell,Texas,425,48,48425,county,Somervell County,http://www.somervell.co/ +Sonoma,California,97,6,6097,county,Sonoma County,https://sonomacounty.ca.gov/ +South Central Connecticut,Connecticut,170,9,9170,planning region,South Central Connecticut Planning Region,"https://en.wikipedia.org/wiki/South_Central_Connecticut_Planning_Region,_Connecticut" +Southampton,Virginia,175,51,51175,county,Southampton County,https://www.southamptoncounty.org/ +Southeastern Connecticut,Connecticut,180,9,9180,planning region,Southeastern Connecticut Planning Region,https://portal.ct.gov/OPM/IGPP/ORG/Planning-Regions/Planning-Regions---Overview +Spalding,Georgia,255,13,13255,county,Spalding County,https://www.spaldingcounty.com/ +Spartanburg,South Carolina,83,45,45083,county,Spartanburg County,https://www.spartanburgcounty.org/ +Spencer,Indiana,147,18,18147,county,Spencer County,https://spencercounty.in.gov/ +Spencer,Kentucky,215,21,21215,county,Spencer County,https://www.spencercountyky.gov/ +Spink,South Dakota,115,46,46115,county,Spink County,https://www.spinkcounty-sd.org/ +Spokane,Washington,63,53,53063,county,Spokane County,https://www.spokanecounty.org/ +Spotsylvania,Virginia,177,51,51177,county,Spotsylvania County,https://www.spotsylvania.va.us/ +St. Bernard,Louisiana,87,22,22087,parish,St. Bernard Parish,https://www.sbpg.net/ +St. Charles,Louisiana,89,22,22089,parish,St. Charles Parish,https://www.stcharlesparish.gov/ +St. Charles,Missouri,183,29,29183,county,St. Charles County,https://www.sccmo.org/ +St. Clair,Alabama,115,1,1115,county,St. Clair County,https://www.stclairco.com/ +St. Clair,Illinois,163,17,17163,county,St. Clair County,https://www.co.st-clair.il.us/ +St. Clair,Missouri,185,29,29185,county,St. Clair County,https://www.stclaircomo.com/ +St. Clair,Michigan,147,26,26147,county,St. Clair County,https://www.stclaircounty.org/ +St. Croix,Wisconsin,109,55,55109,county,St. Croix County,https://www.sccwi.gov/ +St. Francis,Arkansas,123,5,5123,county,St. Francis County,http://stfranciscountyar.org/ +St. Francois,Missouri,187,29,29187,county,St. Francois County,https://www.sfcgov.org/ +St. Helena,Louisiana,91,22,22091,parish,St. Helena Parish,https://www.louisiana.gov/local-louisiana/st-helena-parish +St. James,Louisiana,93,22,22093,parish,St. James Parish,https://www.stjamesla.com/ +St. John the Baptist,Louisiana,95,22,22095,parish,St. John the Baptist Parish,https://www.sjbparish.gov/Home +St. Johns,Florida,109,12,12109,county,St. Johns County,http://www.co.st-johns.fl.us/ +St. Joseph,Indiana,141,18,18141,county,St. Joseph County,https://www.sjcindiana.com/ +St. Joseph,Michigan,149,26,26149,county,St. Joseph County,https://www.stjosephcountymi.org/ +St. Landry,Louisiana,97,22,22097,parish,St. Landry Parish,https://www.louisiana.gov/local-louisiana/st-landry-parish +St. Lawrence,New York,89,36,36089,county,St. Lawrence County,https://stlawco.gov/ +St. Louis City,Missouri,510,29,29510,city,St. Louis city,https://www.stlouis-mo.gov/ +St. Louis,Minnesota,137,27,27137,county,St. Louis County,https://www.stlouiscountymn.gov/ +St. Louis,Missouri,189,29,29189,county,St. Louis County,https://stlouiscountymo.gov/ +St. Lucie,Florida,111,12,12111,county,St. Lucie County,https://www.stlucieco.gov/ +St. Martin,Louisiana,99,22,22099,parish,St. Martin Parish,https://www.stmartinparish.net/ +St. Mary,Louisiana,101,22,22101,parish,St. Mary Parish,https://www.stmaryparishla.gov/ +St. Mary's,Maryland,37,24,24037,county,St. Mary's County,https://www.stmaryscountymd.gov/ +St. Tammany,Louisiana,103,22,22103,parish,St. Tammany Parish,http://www.stpgov.org/ +Stafford,Virginia,179,51,51179,county,Stafford County,https://staffordcountyva.gov/ +Stafford,Kansas,185,20,20185,county,Stafford County,https://www.staffordcounty.org/ +Stanislaus,California,99,6,6099,county,Stanislaus County,https://www.stancounty.com/ +Stanley,South Dakota,117,46,46117,county,Stanley County,https://www.stanleycounty.org/ +Stanly,North Carolina,167,37,37167,county,Stanly County,https://www.stanlycountync.gov/ +Stanton,Nebraska,167,31,31167,county,Stanton County,https://stantoncounty.nebraska.gov/ +Stanton,Kansas,187,20,20187,county,Stanton County,http://www.stantoncountyks.com/ +Stark,Illinois,175,17,17175,county,Stark County,https://www.starkco.illinois.gov/ +Stark,North Dakota,89,38,38089,county,Stark County,https://www.starkcountynd.gov/ +Stark,Ohio,151,39,39151,county,Stark County,https://www.starkcountyohio.gov/ +Starke,Indiana,149,18,18149,county,Starke County,https://starke.in.gov/ +Starr,Texas,427,48,48427,county,Starr County,https://www.co.starr.tx.us/ +Staunton,Virginia,790,51,51790,city,Staunton city,https://www.ci.staunton.va.us/ +Ste. Genevieve,Missouri,186,29,29186,county,Ste. Genevieve County,https://www.stegencounty.org/ +Stearns,Minnesota,145,27,27145,county,Stearns County,https://www.stearnscountymn.gov/ +Steele,North Dakota,91,38,38091,county,Steele County,https://www.co.steele.nd.us/ +Steele,Minnesota,147,27,27147,county,Steele County,https://www.steelecountymn.gov/ +Stephens,Georgia,257,13,13257,county,Stephens County,https://stephenscountyga.gov/ +Stephens,Texas,429,48,48429,county,Stephens County,https://www.co.stephens.tx.us/ +Stephens,Oklahoma,137,40,40137,county,Stephens County,https://oklahoma.gov/okdhs/library/resources/stephensrd211.html +Stephenson,Illinois,177,17,17177,county,Stephenson County,http://stephensoncountyil.gov/ +Sterling,Texas,431,48,48431,county,Sterling County,https://www.co.sterling.tx.us/ +Steuben,New York,101,36,36101,county,Steuben County,https://www.steubencountyny.gov/ +Steuben,Indiana,151,18,18151,county,Steuben County,https://www.co.steuben.in.us/ +Stevens,Washington,65,53,53065,county,Stevens County,https://www.stevenscountywa.gov/ +Stevens,Kansas,189,20,20189,county,Stevens County,http://stevenscoks.org/ +Stevens,Minnesota,149,27,27149,county,Stevens County,https://www.co.stevens.mn.us/ +Stewart,Georgia,259,13,13259,county,Stewart County,http://stewartcountyga.gov/ +Stewart,Tennessee,161,47,47161,county,Stewart County,https://www.stewartcogov.com/ +Stillwater,Montana,95,30,30095,county,Stillwater County,https://www.stillwatercountymt.gov/ +Stoddard,Missouri,207,29,29207,county,Stoddard County,https://dps.mo.gov/dir/programs/cvsu/counties/stoddard.php +Stokes,North Carolina,169,37,37169,county,Stokes County,https://www.co.stokes.nc.us/ +Stone,Arkansas,137,5,5137,county,Stone County,https://stonecountyar.gov/ +Stone,Missouri,209,29,29209,county,Stone County,http://www.stoneco-mo.us/ +Stone,Mississippi,131,28,28131,county,Stone County,https://stonecountyms.gov/ +Stonewall,Texas,433,48,48433,county,Stonewall County,https://www.stonewallcounty.org/ +Storey,Nevada,29,32,32029,county,Storey County,https://www.storeycounty.org/ +Story,Iowa,169,19,19169,county,Story County,https://www.storycountyiowa.gov/ +Strafford,New Hampshire,17,33,33017,county,Strafford County,https://www.co.strafford.nh.us/ +Stutsman,North Dakota,93,38,38093,county,Stutsman County,https://www.stutsmancounty.gov/ +Sublette,Wyoming,35,56,56035,county,Sublette County,https://www.sublettecountywy.gov/ +Suffolk,Massachusetts,25,25,25025,county,Suffolk County,https://www.mass.gov/orgs/sjc-clerks-office-for-the-county-of-suffolk +Suffolk,New York,103,36,36103,county,Suffolk County,https://suffolkcountyny.gov/ +Suffolk,Virginia,800,51,51800,city,Suffolk city,https://www.suffolkva.us/ +Sullivan,Tennessee,163,47,47163,county,Sullivan County,https://sullivancountytn.gov/ +Sullivan,Pennsylvania,113,42,42113,county,Sullivan County,https://www.sullivancountypa.gov/ +Sullivan,Missouri,211,29,29211,county,Sullivan County,https://www.mocounties.com/sullivan-county +Sullivan,Indiana,153,18,18153,county,Sullivan County,https://www.sullivancounty.in.gov/ +Sullivan,New Hampshire,19,33,33019,county,Sullivan County,https://www.sullivancountynh.gov/ +Sullivan,New York,105,36,36105,county,Sullivan County,https://sullivanny.us/home +Sully,South Dakota,119,46,46119,county,Sully County,https://www.sullycounty.net/ +Summers,West Virginia,89,54,54089,county,Summers County,https://www.summerscountywv.gov/ +Summit,Utah,43,49,49043,county,Summit County,https://www.summitcounty.org/ +Summit,Ohio,153,39,39153,county,Summit County,https://co.summitoh.net/portal/County-of-Summit-Ohio.html +Summit,Colorado,117,8,8117,county,Summit County,https://www.summitcountyco.gov/ +Sumner,Kansas,191,20,20191,county,Sumner County,https://www.co.sumner.ks.us/ +Sumner,Tennessee,165,47,47165,county,Sumner County,https://sumnercountytn.gov/ +Sumter,Alabama,119,1,1119,county,Sumter County,https://sumtercountyal.com/ +Sumter,Florida,119,12,12119,county,Sumter County,https://www.sumtercountyfl.gov/ +Sumter,South Carolina,85,45,45085,county,Sumter County,https://www.sumtercountysc.org/ +Sumter,Georgia,261,13,13261,county,Sumter County,https://www.sumtercountyga.us/ +Sunflower,Mississippi,133,28,28133,county,Sunflower County,https://www.sunflowercounty.ms.gov/ +Surry,North Carolina,171,37,37171,county,Surry County,https://www.co.surry.nc.us/ +Surry,Virginia,181,51,51181,county,Surry County,https://www.surrycountyva.gov/ +Susquehanna,Pennsylvania,115,42,42115,county,Susquehanna County,https://www.susqco.com/ +Sussex,Virginia,183,51,51183,county,Sussex County,https://www.sussexcountyva.gov/ +Sussex,New Jersey,37,34,34037,county,Sussex County,https://www.sussex.nj.us/ +Sussex,Delaware,5,10,10005,county,Sussex County,https://sussexcountyde.gov/ +Sutter,California,101,6,6101,county,Sutter County,https://www.suttercounty.org/ +Sutton,Texas,435,48,48435,county,Sutton County,https://www.co.sutton.tx.us/ +Suwannee,Florida,121,12,12121,county,Suwannee County,https://suwanneecountyfl.gov/ +Swain,North Carolina,173,37,37173,county,Swain County,https://www.swaincountync.gov/ +Sweet Grass,Montana,97,30,30097,county,Sweet Grass County,https://sweetgrasscountygov.com/ +Sweetwater,Wyoming,37,56,56037,county,Sweetwater County,https://www.sweetwatercountywy.gov/ +Swift,Minnesota,151,27,27151,county,Swift County,https://www.swiftcounty.com/ +Swisher,Texas,437,48,48437,county,Swisher County,https://www.co.swisher.tx.us/ +Switzerland,Indiana,155,18,18155,county,Switzerland County,https://www.switzerland-county.com/ +Talbot,Georgia,263,13,13263,county,Talbot County,https://talbotcountyga.org/ +Talbot,Maryland,41,24,24041,county,Talbot County,https://talbotcountymd.gov/ +Taliaferro,Georgia,265,13,13265,county,Taliaferro County,https://taliaferrocountyga.org/ +Talladega,Alabama,121,1,1121,county,Talladega County,https://www.talladegacountyal.org/ +Tallahatchie,Mississippi,135,28,28135,county,Tallahatchie County,https://www.mssupervisors.org/ms-counties/tallahatchie +Tallapoosa,Alabama,123,1,1123,county,Tallapoosa County,https://tallaco.com/ +Tama,Iowa,171,19,19171,county,Tama County,https://www.tamacounty.iowa.gov/ +Taney,Missouri,213,29,29213,county,Taney County,https://www.taneycounty.org/ +Tangipahoa,Louisiana,105,22,22105,parish,Tangipahoa Parish,https://tangipahoa.org/ +Taos,New Mexico,55,35,35055,county,Taos County,https://www.taoscounty.org/ +Tarrant,Texas,439,48,48439,county,Tarrant County,https://www.tarrantcountytx.gov/en.html +Tate,Mississippi,137,28,28137,county,Tate County,https://www.tatecountygov.com/ +Tattnall,Georgia,267,13,13267,county,Tattnall County,https://www.tattnallcountyga.com/ +Taylor,Texas,441,48,48441,county,Taylor County,https://www.taylorcounty.texas.gov/ +Taylor,Florida,123,12,12123,county,Taylor County,https://www.taylorcountygov.com/ +Taylor,Wisconsin,119,55,55119,county,Taylor County,https://co.taylor.wi.us/ +Taylor,Kentucky,217,21,21217,county,Taylor County,https://taylorcountyky.gov/ +Taylor,Georgia,269,13,13269,county,Taylor County,https://taylorcountyga.com/ +Taylor,Iowa,173,19,19173,county,Taylor County,https://www.taylorcounty.iowa.gov/author/goodyis/ +Taylor,West Virginia,91,54,54091,county,Taylor County,https://www.wvcountytaylor.com/copy-of-home +Tazewell,Virginia,185,51,51185,county,Tazewell County,http://tazewellcountyva.org/ +Tazewell,Illinois,179,17,17179,county,Tazewell County,https://www.tazewell-il.gov/ +Tehama,California,103,6,6103,county,Tehama County,https://www.co.tehama.ca.us/ +Telfair,Georgia,271,13,13271,county,Telfair County,http://www.telfairco.org/ +Teller,Colorado,119,8,8119,county,Teller County,https://www.co.teller.co.us/ +Tensas,Louisiana,107,22,22107,parish,Tensas Parish,https://www.louisiana.gov/local-louisiana/tensas-parish +Terrebonne,Louisiana,109,22,22109,parish,Terrebonne Parish,https://www.tpcg.org/ +Terrell,Texas,443,48,48443,county,Terrell County,https://www.co.terrell.tx.us/ +Terrell,Georgia,273,13,13273,county,Terrell County,https://terrellcountyga.gov/ +Terry,Texas,445,48,48445,county,Terry County,https://www.co.terry.tx.us/ +Teton,Montana,99,30,30099,county,Teton County,https://tetoncountymt.gov/ +Teton,Idaho,81,16,16081,county,Teton County,https://www.tetoncountyidaho.gov/ +Teton,Wyoming,39,56,56039,county,Teton County,https://www.tetoncountywy.gov/ +Texas,Oklahoma,139,40,40139,county,Texas County,https://texas.okcounties.org/ +Texas,Missouri,215,29,29215,county,Texas County,https://www.texascountymissouri.gov/ +Thayer,Nebraska,169,31,31169,county,Thayer County,https://thayercountyne.gov/ +Thomas,Georgia,275,13,13275,county,Thomas County,https://thomascountyboc.org/ +Thomas,Kansas,193,20,20193,county,Thomas County,https://thomascountyks.gov/ +Thomas,Nebraska,171,31,31171,county,Thomas County,https://www.thomascountynebraska.us/ +Throckmorton,Texas,447,48,48447,county,Throckmorton County,https://www.throckmortoncounty.org/ +Thurston,Washington,67,53,53067,county,Thurston County,https://www.thurstoncountywa.gov/ +Thurston,Nebraska,173,31,31173,county,Thurston County,http://thurstoncountynebraska.us/ +Tift,Georgia,277,13,13277,county,Tift County,https://www.tiftcounty.org/ +Tillamook,Oregon,57,41,41057,county,Tillamook County,https://www.co.tillamook.or.us/home +Tillman,Oklahoma,141,40,40141,county,Tillman County,https://tillman.okcounties.org/ +Tioga,Pennsylvania,117,42,42117,county,Tioga County,https://www.tiogacountypa.us/ +Tioga,New York,107,36,36107,county,Tioga County,https://www.tiogacountyny.com/ +Tippah,Mississippi,139,28,28139,county,Tippah County,http://www.co.tippah.ms.us/ +Tippecanoe,Indiana,157,18,18157,county,Tippecanoe County,https://www.tippecanoe.in.gov/ +Tipton,Tennessee,167,47,47167,county,Tipton County,https://tiptonco.com/ +Tipton,Indiana,159,18,18159,county,Tipton County,https://www.tiptongov.com/county/ +Tishomingo,Mississippi,141,28,28141,county,Tishomingo County,https://www.co.tishomingo.ms.us/ +Titus,Texas,449,48,48449,county,Titus County,http://www.co.titus.tx.us/ +Todd,Kentucky,219,21,21219,county,Todd County,https://toddcounty.ky.gov/Pages/index.aspx +Todd,South Dakota,121,46,46121,county,Todd County,https://ujs.sd.gov/Sixth_Circuit/Links/Counties.aspx?Hmn1tWpd69rBYdhIw7dgITLe58Shr%2B8w4224dOZ%2BnOE%3D +Todd,Minnesota,153,27,27153,county,Todd County,https://www.co.todd.mn.us/ +Tom Green,Texas,451,48,48451,county,Tom Green County,https://www.tomgreencountytx.gov/ +Tompkins,New York,109,36,36109,county,Tompkins County,https://www.tompkinscountyny.gov/home +Tooele,Utah,45,49,49045,county,Tooele County,https://tooeleco.org/ +Toole,Montana,101,30,30101,county,Toole County,https://www.toolecountymt.gov/ +Toombs,Georgia,279,13,13279,county,Toombs County,https://www.toombscountyga.gov/ +Torrance,New Mexico,57,35,35057,county,Torrance County,http://www.torrancecountynm.org/ +Towner,North Dakota,95,38,38095,county,Towner County,https://www.tccounty.com/ +Towns,Georgia,281,13,13281,county,Towns County,http://www.townscountyga.org/ +Traill,North Dakota,97,38,38097,county,Traill County,https://www.co.traill.nd.us/ +Transylvania,North Carolina,175,37,37175,county,Transylvania County,https://www.transylvaniacounty.org/ +Traverse,Minnesota,155,27,27155,county,Traverse County,https://www.co.traverse.mn.us/ +Travis,Texas,453,48,48453,county,Travis County,https://www.traviscountytx.gov/ +Treasure,Montana,103,30,30103,county,Treasure County,https://courts.mt.gov/external/selfhelp/resources/treasure.pdf +Trego,Kansas,195,20,20195,county,Trego County,https://www.tregocountyks.com/ +Trempealeau,Wisconsin,121,55,55121,county,Trempealeau County,https://co.trempealeau.wi.us/ +Treutlen,Georgia,283,13,13283,county,Treutlen County,https://treutlencountygov.com/ +Trigg,Kentucky,221,21,21221,county,Trigg County,https://triggcounty.ky.gov/ +Trimble,Kentucky,223,21,21223,county,Trimble County,https://trimblecounty.ky.gov/ +Trinity,California,105,6,6105,county,Trinity County,https://www.trinitycounty.org/ +Trinity,Texas,455,48,48455,county,Trinity County,https://www.co.trinity.tx.us/ +Tripp,South Dakota,123,46,46123,county,Tripp County,https://trippcounty.us/ +Troup,Georgia,285,13,13285,county,Troup County,https://www.troupcountyga.gov/ +Trousdale,Tennessee,169,47,47169,county,Trousdale County,https://www.trousdalecountytn.gov/ +Trumbull,Ohio,155,39,39155,county,Trumbull County,https://www.co.trumbull.oh.us/ +Tucker,West Virginia,93,54,54093,county,Tucker County,https://tuckercounty.wv.gov/ +Tulare,California,107,6,6107,county,Tulare County,https://tularecounty.ca.gov/county/ +Tulsa,Oklahoma,143,40,40143,county,Tulsa County,https://www.tulsacounty.org/ +Tunica,Mississippi,143,28,28143,county,Tunica County,https://www.tunicacountymississippi.com/ +Tuolumne,California,109,6,6109,county,Tuolumne County,https://www.tuolumnecounty.ca.gov/ +Turner,Georgia,287,13,13287,county,Turner County,http://www.turnercountygeorgia.com/ +Turner,South Dakota,125,46,46125,county,Turner County,https://turner.sdcounties.org/ +Tuscaloosa,Alabama,125,1,1125,county,Tuscaloosa County,https://www.tuscco.com/ +Tuscarawas,Ohio,157,39,39157,county,Tuscarawas County,https://www.co.tuscarawas.oh.us/ +Tuscola,Michigan,157,26,26157,county,Tuscola County,https://www.tuscolacounty.org/ +Twiggs,Georgia,289,13,13289,county,Twiggs County,https://www.twiggscounty.us/ +Twin Falls,Idaho,83,16,16083,county,Twin Falls County,https://twinfallscounty.org/ +Tyler,Texas,457,48,48457,county,Tyler County,https://www.co.tyler.tx.us/ +Tyler,West Virginia,95,54,54095,county,Tyler County,https://tylercountywv.com/ +Tyrrell,North Carolina,177,37,37177,county,Tyrrell County,http://tyrrellcounty.org/ +Uinta,Wyoming,41,56,56041,county,Uinta County,https://www.uintacounty.com/ +Uintah,Utah,47,49,49047,county,Uintah County,https://www.uintah.utah.gov/ +Ulster,New York,111,36,36111,county,Ulster County,https://ulstercountyny.gov/ +Umatilla,Oregon,59,41,41059,county,Umatilla County,https://www.co.umatilla.or.us/ +Unicoi,Tennessee,171,47,47171,county,Unicoi County,https://unicoicountytn.com/ +Union,Arkansas,139,5,5139,county,Union County,https://www.unioncountyar.com/ +Union,Oregon,61,41,41061,county,Union County,https://union-county.org/ +Union,Indiana,161,18,18161,county,Union County,https://www.unioncountyin.org/ +Union,North Carolina,179,37,37179,county,Union County,https://www.unioncountync.gov/ +Union,Illinois,181,17,17181,county,Union County,https://www.facebook.com/UnionCountyIllinois/ +Union,Ohio,159,39,39159,county,Union County,https://www.unioncountyohio.gov/ +Union,South Carolina,87,45,45087,county,Union County,https://gearupunionsc.com/ +Union,Kentucky,225,21,21225,county,Union County,https://www.unioncountyky.org/ +Union,Florida,125,12,12125,county,Union County,https://unioncounty-fl.gov/our-mission/ +Union,Pennsylvania,119,42,42119,county,Union County,https://unioncountypa.org/ +Union,South Dakota,127,46,46127,county,Union County,https://unioncountysd.org/ +Union,Iowa,175,19,19175,county,Union County,https://unioncountyiowa.gov/ +Union,Louisiana,111,22,22111,parish,Union Parish,https://www.louisiana.gov/local-louisiana/union-parish +Union,New Jersey,39,34,34039,county,Union County,https://ucnj.org/ +Union,Georgia,291,13,13291,county,Union County,https://www.unioncountyga.gov/ +Union,Mississippi,145,28,28145,county,Union County,https://www.mssupervisors.org/ms-counties/union +Union,Tennessee,173,47,47173,county,Union County,https://www.unioncountytn.gov/ +Union,New Mexico,59,35,35059,county,Union County,https://unionnm.us/ +Upshur,Texas,459,48,48459,county,Upshur County,https://www.countyofupshur.com/ +Upshur,West Virginia,97,54,54097,county,Upshur County,https://www.upshurcounty.org/ +Upson,Georgia,293,13,13293,county,Upson County,https://www.upsoncountyga.org/ +Upton,Texas,461,48,48461,county,Upton County,https://www.co.upton.tx.us/ +Utah,Utah,49,49,49049,county,Utah County,https://www.utahcounty.gov/ +Uvalde,Texas,463,48,48463,county,Uvalde County,https://www.uvaldetx.gov/ +Val Verde,Texas,465,48,48465,county,Val Verde County,https://valverdecounty.texas.gov/ +Valencia,New Mexico,61,35,35061,county,Valencia County,https://www.co.valencia.nm.us/ +Valley,Idaho,85,16,16085,county,Valley County,https://www.co.valley.id.us/ +Valley,Nebraska,175,31,31175,county,Valley County,https://co.valley.ne.us/ +Valley,Montana,105,30,30105,county,Valley County,https://www.valleycountymt.net/ +Van Buren,Tennessee,175,47,47175,county,Van Buren County,https://vanburencountytn.com/ +Van Buren,Michigan,159,26,26159,county,Van Buren County,https://www.vanburencountymi.gov/ +Van Buren,Iowa,177,19,19177,county,Van Buren County,https://www.vanburencounty.iowa.gov/ +Van Buren,Arkansas,141,5,5141,county,Van Buren County,https://local.arkansas.gov/local.php?agency=Van%20buren%20County +Van Wert,Ohio,161,39,39161,county,Van Wert County,https://www.vanwertcountyohio.gov/ +Van Zandt,Texas,467,48,48467,county,Van Zandt County,https://www.vanzandtcounty.org/ +Vance,North Carolina,181,37,37181,county,Vance County,https://www.vancecounty.org/ +Vanderburgh,Indiana,163,18,18163,county,Vanderburgh County,https://www.vanderburghgov.org/ +Venango,Pennsylvania,121,42,42121,county,Venango County,https://co.venango.pa.us/ +Ventura,California,111,6,6111,county,Ventura County,https://www.ventura.org/ +Vermilion,Illinois,183,17,17183,county,Vermilion County,https://www.vercounty.org/ +Vermilion,Louisiana,113,22,22113,parish,Vermilion Parish,https://www.louisiana.gov/local-louisiana/vermilion-parish +Vermillion,Indiana,165,18,18165,county,Vermillion County,https://www.vermilliongov.us/ +Vernon,Louisiana,115,22,22115,parish,Vernon Parish,https://www.louisiana.gov/local-louisiana/vernon-parish +Vernon,Missouri,217,29,29217,county,Vernon County,https://vernoncountymo.org/ +Vernon,Wisconsin,123,55,55123,county,Vernon County,https://www.vernoncounty.org/ +Victoria,Texas,469,48,48469,county,Victoria County,https://www.vctx.org/ +Vigo,Indiana,167,18,18167,county,Vigo County,https://www.vigocounty.in.gov/ +Vilas,Wisconsin,125,55,55125,county,Vilas County,https://www.vilascountywi.gov/ +Vinton,Ohio,163,39,39163,county,Vinton County,http://www.vintoncounty.com/ +Virginia Beach,Virginia,810,51,51810,city,Virginia Beach city,https://virginiabeach.gov/ +Volusia,Florida,127,12,12127,county,Volusia County,https://www.volusia.org/ +Wabash,Illinois,185,17,17185,county,Wabash County,https://www.ilsos.gov/departments/archives/IRAD/wabash.html +Wabash,Indiana,169,18,18169,county,Wabash County,https://www.wabashcounty.in.gov/ +Wabasha,Minnesota,157,27,27157,county,Wabasha County,https://www.co.wabasha.mn.us/ +Wabaunsee,Kansas,197,20,20197,county,Wabaunsee County,https://www.wbcounty.org/ +Wadena,Minnesota,159,27,27159,county,Wadena County,http://www.co.wadena.mn.us/ +Wagoner,Oklahoma,145,40,40145,county,Wagoner County,https://www.ok.gov/wagonercounty/ +Wahkiakum,Washington,69,53,53069,county,Wahkiakum County,https://www.co.wahkiakum.wa.us/ +Wake,North Carolina,183,37,37183,county,Wake County,https://www.wake.gov/ +Wakulla,Florida,129,12,12129,county,Wakulla County,https://www.mywakulla.com/ +Waldo,Maine,27,23,23027,county,Waldo County,https://www.waldocountyme.gov/ +Walker,Alabama,127,1,1127,county,Walker County,https://walkercountyal.us/ +Walker,Georgia,295,13,13295,county,Walker County,https://walkercountyga.gov/ +Walker,Texas,471,48,48471,county,Walker County,https://www.co.walker.tx.us/ +Walla Walla,Washington,71,53,53071,county,Walla Walla County,https://www.co.walla-walla.wa.us/ +Wallace,Kansas,199,20,20199,county,Wallace County,https://wallacecountyks.gov/ +Waller,Texas,473,48,48473,county,Waller County,https://www.co.waller.tx.us/ +Wallowa,Oregon,63,41,41063,county,Wallowa County,https://www.co.wallowa.or.us/ +Walsh,North Dakota,99,38,38099,county,Walsh County,https://walshcountynd.com/ +Walthall,Mississippi,147,28,28147,county,Walthall County,https://www.walthallchamber.com/ +Walton,Georgia,297,13,13297,county,Walton County,https://www.waltoncountyga.gov/ +Walton,Florida,131,12,12131,county,Walton County,https://www.co.walton.fl.us/ +Walworth,South Dakota,129,46,46129,county,Walworth County,https://walworthco.org/ +Walworth,Wisconsin,127,55,55127,county,Walworth County,https://www.co.walworth.wi.us/ +Wapello,Iowa,179,19,19179,county,Wapello County,https://www.wapellocounty.org/ +Ward,North Dakota,101,38,38101,county,Ward County,https://www.co.ward.nd.us/ +Ward,Texas,475,48,48475,county,Ward County,https://www.co.ward.tx.us/ +Ware,Georgia,299,13,13299,county,Ware County,https://www.warecountyga.gov/ +Warren,North Carolina,185,37,37185,county,Warren County,https://www.warrencountync.com/ +Warren,New York,113,36,36113,county,Warren County,https://www.warrencountyny.gov/home +Warren,Illinois,187,17,17187,county,Warren County,http://www.warrencountyil.com/ +Warren,Missouri,219,29,29219,county,Warren County,https://warrencountymoclerk.com/ +Warren,Mississippi,149,28,28149,county,Warren County,https://co.warren.ms.us/ +Warren,Pennsylvania,123,42,42123,county,Warren County,https://warrencountypa.gov/ +Warren,Indiana,171,18,18171,county,Warren County,https://www.warrencounty.in.gov/ +Warren,Kentucky,227,21,21227,county,Warren County,https://www.warrencountyky.gov/ +Warren,New Jersey,41,34,34041,county,Warren County,https://www.warrencountynj.gov/ +Warren,Iowa,181,19,19181,county,Warren County,https://www.warrencountyia.gov/ +Warren,Tennessee,177,47,47177,county,Warren County,https://www.warrencountytn.gov/ +Warren,Virginia,187,51,51187,county,Warren County,https://warrencountyva.gov/ +Warren,Georgia,301,13,13301,county,Warren County,https://www.warrencountyga.com/ +Warren,Ohio,165,39,39165,county,Warren County,https://www.co.warren.oh.us/ +Warrick,Indiana,173,18,18173,county,Warrick County,https://www.warrickcounty.gov/ +Wasatch,Utah,51,49,49051,county,Wasatch County,https://wasatch.utah.gov/ +Wasco,Oregon,65,41,41065,county,Wasco County,https://www.co.wasco.or.us/ +Waseca,Minnesota,161,27,27161,county,Waseca County,https://www.wasecacounty.gov/ +Washakie,Wyoming,43,56,56043,county,Washakie County,https://www.washakiecounty.net/ +Washburn,Wisconsin,129,55,55129,county,Washburn County,https://www.co.washburn.wi.us/ +Washington,Florida,133,12,12133,county,Washington County,https://washingtonfl.com/ +Washington,Idaho,87,16,16087,county,Washington County,https://co.washington.id.us/ +Washington,Illinois,189,17,17189,county,Washington County,https://washingtonco.illinois.gov/ +Washington,Utah,53,49,49053,county,Washington County,https://www.washco.utah.gov/ +Washington,Mississippi,151,28,28151,county,Washington County,http://www.washingtoncounty.ms/ +Washington,Iowa,183,19,19183,county,Washington County,https://washingtoncounty.iowa.gov/ +Washington,Georgia,303,13,13303,county,Washington County,https://washingtoncountyga.gov/ +Washington,Tennessee,179,47,47179,county,Washington County,https://www.washingtoncountytn.org/ +Washington,Vermont,23,50,50023,county,Washington County,http://vermont.gov/ +Washington,Minnesota,163,27,27163,county,Washington County,https://www.co.washington.mn.us/ +Washington,Wisconsin,131,55,55131,county,Washington County,https://www.washcowisco.gov/ +Washington,Oklahoma,147,40,40147,county,Washington County,http://countycourthouse.org/ +Washington,Rhode Island,9,44,44009,county,Washington County,https://www.ri.gov/ +Washington,Kentucky,229,21,21229,county,Washington County,https://www.washingtoncountyky.com/ +Washington,Nebraska,177,31,31177,county,Washington County,http://www.co.washington.ne.us/ +Washington,Louisiana,117,22,22117,parish,Washington Parish,https://www.louisiana.gov/local-louisiana/washington-parish +Washington,Texas,477,48,48477,county,Washington County,https://www.co.washington.tx.us/ +Washington,Oregon,67,41,41067,county,Washington County,https://www.washingtoncountyor.gov/ +Washington,Ohio,167,39,39167,county,Washington County,https://www.washingtongov.org/ +Washington,Maine,29,23,23029,county,Washington County,https://www.washington.maine.gov/ +Washington,Pennsylvania,125,42,42125,county,Washington County,https://www.co.washington.pa.us/ +Washington,New York,115,36,36115,county,Washington County,https://www.washingtoncountyny.gov/ +Washington,Missouri,221,29,29221,county,Washington County,https://www.washingtoncountymo.us/ +Washington,North Carolina,187,37,37187,county,Washington County,https://washconc.org/ +Washington,Virginia,191,51,51191,county,Washington County,https://www.washcova.com/ +Washington,Indiana,175,18,18175,county,Washington County,https://www.washingtoncounty.in.gov/ +Washington,Colorado,121,8,8121,county,Washington County,https://washingtoncounty.colorado.gov/ +Washington,Maryland,43,24,24043,county,Washington County,https://www.washco-md.net/ +Washington,Arkansas,143,5,5143,county,Washington County,https://www.washingtoncountyar.gov/ +Washington,Kansas,201,20,20201,county,Washington County,https://washingtoncountyks.gov/ +Washington,Alabama,129,1,1129,county,Washington County,https://wcalabama.com/ +Washita,Oklahoma,149,40,40149,county,Washita County,https://oklahoma.gov/okdhs/library/resources/washitard211.html +Washoe,Nevada,31,32,32031,county,Washoe County,https://www.washoecounty.gov/ +Washtenaw,Michigan,161,26,26161,county,Washtenaw County,https://www.washtenaw.org/ +Watauga,North Carolina,189,37,37189,county,Watauga County,https://www.wataugacounty.org/ +Watonwan,Minnesota,165,27,27165,county,Watonwan County,https://www.co.watonwan.mn.us/ +Waukesha,Wisconsin,133,55,55133,county,Waukesha County,https://www.waukeshacounty.gov/ +Waupaca,Wisconsin,135,55,55135,county,Waupaca County,https://www.waupacacounty-wi.gov/ +Waushara,Wisconsin,137,55,55137,county,Waushara County,https://www.co.waushara.wi.us/ +Wayne,Michigan,163,26,26163,county,Wayne County,https://www.waynecounty.com/ +Wayne,New York,117,36,36117,county,Wayne County,https://web.co.wayne.ny.us/ +Wayne,West Virginia,99,54,54099,county,Wayne County,https://www.waynecountywv.org/ +Wayne,Kentucky,231,21,21231,county,Wayne County,https://waynecounty.ky.gov/Pages/default.aspx +Wayne,Nebraska,179,31,31179,county,Wayne County,https://www.waynecountyne.gov/ +Wayne,Indiana,177,18,18177,county,Wayne County,https://co.wayne.in.us/ +Wayne,Missouri,223,29,29223,county,Wayne County,https://waynecountycollector.com/ +Wayne,Iowa,185,19,19185,county,Wayne County,https://waynecounty.iowa.gov/ +Wayne,Tennessee,181,47,47181,county,Wayne County,http://www.waynecountytn.org/ +Wayne,Georgia,305,13,13305,county,Wayne County,https://www.waynecountyga.us/ +Wayne,North Carolina,191,37,37191,county,Wayne County,https://www.waynegov.com/ +Wayne,Utah,55,49,49055,county,Wayne County,https://waynecountyutah.org/ +Wayne,Illinois,191,17,17191,county,Wayne County,https://www.ilsos.gov/departments/archives/IRAD/wayne.html +Wayne,Ohio,169,39,39169,county,Wayne County,https://www.wayneohio.org/ +Wayne,Mississippi,153,28,28153,county,Wayne County,http://www.waynecounty.ms/ +Wayne,Pennsylvania,127,42,42127,county,Wayne County,https://waynecountypa.gov/ +Waynesboro,Virginia,820,51,51820,city,Waynesboro city,https://www.waynesboro.va.us/ +Weakley,Tennessee,183,47,47183,county,Weakley County,https://www.weakleycountytn.gov/ +Webb,Texas,479,48,48479,county,Webb County,https://www.webbcountytx.gov/ +Weber,Utah,57,49,49057,county,Weber County,https://webercountyutah.gov/ +Webster,Kentucky,233,21,21233,county,Webster County,https://webstercountyclerk.ky.gov/Pages/default.aspx +Webster,Nebraska,181,31,31181,county,Webster County,https://co.webster.ne.us/ +Webster,Louisiana,119,22,22119,parish,Webster Parish,https://www.louisiana.gov/local-louisiana/webster-parish +Webster,Iowa,187,19,19187,county,Webster County,https://www.webstercountyia.gov/ +Webster,Missouri,225,29,29225,county,Webster County,https://webstercountymo.gov/ +Webster,West Virginia,101,54,54101,county,Webster County,https://webstercounty.wv.gov/ +Webster,Georgia,307,13,13307,county,Webster County,https://webstercountyga.org/ +Webster,Mississippi,155,28,28155,county,Webster County,http://www.webstercountyms.org/ +Weld,Colorado,123,8,8123,county,Weld County,https://www.weld.gov/ +Wells,Indiana,179,18,18179,county,Wells County,https://wellscounty.org/ +Wells,North Dakota,103,38,38103,county,Wells County,https://www.wellscountynd.com/ +West Baton Rouge,Louisiana,121,22,22121,parish,West Baton Rouge Parish,https://www.wbrparish.org/ +West Carroll,Louisiana,123,22,22123,parish,West Carroll Parish,https://www.louisiana.gov/local-louisiana/west-carroll-parish +West Feliciana,Louisiana,125,22,22125,parish,West Feliciana Parish,https://www.wfparish.org/ +Westchester,New York,119,36,36119,county,Westchester County,https://www.westchestergov.com/ +Western Connecticut,Connecticut,190,9,9190,planning region,Western Connecticut Planning Region,https://portal.ct.gov/OPM/IGPP/ORG/Planning-Regions/Planning-Regions---Overview +Westmoreland,Virginia,193,51,51193,county,Westmoreland County,https://www.westmoreland-county.org/ +Westmoreland,Pennsylvania,129,42,42129,county,Westmoreland County,https://www.co.westmoreland.pa.us/ +Weston,Wyoming,45,56,56045,county,Weston County,https://www.westongov.com/ +Wetzel,West Virginia,103,54,54103,county,Wetzel County,https://www.wetzelwv.com/ +Wexford,Michigan,165,26,26165,county,Wexford County,https://wexfordcounty.org/ +Wharton,Texas,481,48,48481,county,Wharton County,https://www.co.wharton.tx.us/ +Whatcom,Washington,73,53,53073,county,Whatcom County,https://www.whatcomcounty.us/ +Wheatland,Montana,107,30,30107,county,Wheatland County,https://mslservices.mt.gov/Legislative_Snapshot/CountyDetail.aspx?coFIPS=30107 +Wheeler,Oregon,69,41,41069,county,Wheeler County,https://www.wheelercountyoregon.com/ +Wheeler,Texas,483,48,48483,county,Wheeler County,https://www.co.wheeler.tx.us/ +Wheeler,Georgia,309,13,13309,county,Wheeler County,https://www.wheelercounty.org/ +Wheeler,Nebraska,183,31,31183,county,Wheeler County,https://wheelercounty.ne.gov/ +White,Indiana,181,18,18181,county,White County,https://whitecounty.in.gov/ +White,Arkansas,145,5,5145,county,White County,https://www.whitecounty.ar.gov/ +White,Tennessee,185,47,47185,county,White County,https://whitecountytn.gov/contact-0 +White,Georgia,311,13,13311,county,White County,https://www.whitecountyga.gov/home +White,Illinois,193,17,17193,county,White County,https://www.whitecounty-il.gov/ +White Pine,Nevada,33,32,32033,county,White Pine County,https://www.whitepinecounty.net/ +Whiteside,Illinois,195,17,17195,county,Whiteside County,https://www.whitesidecountyil.gov/ +Whitfield,Georgia,313,13,13313,county,Whitfield County,https://www.whitfieldcountyga.com/ +Whitley,Kentucky,235,21,21235,county,Whitley County,http://www.whitleycountyfiscalcourt.com/ +Whitley,Indiana,183,18,18183,county,Whitley County,https://www.whitleygov.com/ +Whitman,Washington,75,53,53075,county,Whitman County,https://www.whitmancounty.org/ +Wibaux,Montana,109,30,30109,county,Wibaux County,https://mslservices.mt.gov/Legislative_Snapshot/CountyDetail.aspx?coFIPS=30109 +Wichita,Kansas,203,20,20203,county,Wichita County,https://www.wichitacounty.org/ +Wichita,Texas,485,48,48485,county,Wichita County,https://wichitacountytx.com/ +Wicomico,Maryland,45,24,24045,county,Wicomico County,https://www.wicomicocounty.org/ +Wilbarger,Texas,487,48,48487,county,Wilbarger County,https://www.co.wilbarger.tx.us/ +Wilcox,Georgia,315,13,13315,county,Wilcox County,http://s955274909.onlinehome.us/ +Wilcox,Alabama,131,1,1131,county,Wilcox County,https://www.sos.alabama.gov/city-county-lookup/wilcox +Wilkes,Georgia,317,13,13317,county,Wilkes County,https://www.wilkescountyga.org/government +Wilkes,North Carolina,193,37,37193,county,Wilkes County,https://wilkescounty.net/ +Wilkin,Minnesota,167,27,27167,county,Wilkin County,https://wilkincounty.gov/ +Wilkinson,Georgia,319,13,13319,county,Wilkinson County,https://www.wilkinsoncounty.net/ +Wilkinson,Mississippi,157,28,28157,county,Wilkinson County,http://www.wilkinson.co.ms.gov/ +Will,Illinois,197,17,17197,county,Will County,https://willcounty.gov/ +Willacy,Texas,489,48,48489,county,Willacy County,https://www.co.willacy.tx.us/ +Williams,North Dakota,105,38,38105,county,Williams County,https://www.williamsnd.com/ +Williams,Ohio,171,39,39171,county,Williams County,https://www.williamscountyoh.gov/ +Williamsburg,South Carolina,89,45,45089,county,Williamsburg County,https://www.williamsburgcounty.sc.gov/ +Williamsburg,Virginia,830,51,51830,city,Williamsburg city,https://www.williamsburgva.gov/ +Williamson,Texas,491,48,48491,county,Williamson County,https://www.wilcotx.gov/ +Williamson,Tennessee,187,47,47187,county,Williamson County,https://williamsoncounty-tn.gov/ +Williamson,Illinois,199,17,17199,county,Williamson County,https://www.williamsoncountyil.gov/ +Wilson,Tennessee,189,47,47189,county,Wilson County,https://www.wilsoncountytn.gov/ +Wilson,Texas,493,48,48493,county,Wilson County,https://www.co.wilson.tx.us/ +Wilson,Kansas,205,20,20205,county,Wilson County,http://www.wilsoncountykansas.org/ +Wilson,North Carolina,195,37,37195,county,Wilson County,https://www.wilsoncountync.gov/ +Winchester,Virginia,840,51,51840,city,Winchester city,https://www.winchesterva.gov/ +Windham,Vermont,25,50,50025,county,Windham County,https://windhamcountyvt.gov/ +Windsor,Vermont,27,50,50027,county,Windsor County,http://www.vermont.gov/ +Winkler,Texas,495,48,48495,county,Winkler County,https://www.co.winkler.tx.us/ +Winn,Louisiana,127,22,22127,parish,Winn Parish,https://www.louisiana.gov/local-louisiana/winn-parish +Winnebago,Wisconsin,139,55,55139,county,Winnebago County,https://www.co.winnebago.wi.us/ +Winnebago,Iowa,189,19,19189,county,Winnebago County,https://www.winnebagocountyiowa.gov/ +Winnebago,Illinois,201,17,17201,county,Winnebago County,https://wincoil.gov/ +Winneshiek,Iowa,191,19,19191,county,Winneshiek County,https://winneshiekcounty.iowa.gov/ +Winona,Minnesota,169,27,27169,county,Winona County,https://www.co.winona.mn.us/ +Winston,Alabama,133,1,1133,county,Winston County,https://www.winstoncountyrevenue.com/ +Winston,Mississippi,159,28,28159,county,Winston County,http://www.winstonms.com/ +Wirt,West Virginia,105,54,54105,county,Wirt County,https://wirtcounty.wv.gov/ +Wise,Virginia,195,51,51195,county,Wise County,https://www.wisecounty.org/ +Wise,Texas,497,48,48497,county,Wise County,https://www.co.wise.tx.us/ +Wolfe,Kentucky,237,21,21237,county,Wolfe County,https://kentucky.gov/government/Pages/AgencyProfile.aspx?Title=Wolfe+County +Wood,West Virginia,107,54,54107,county,Wood County,https://woodcountywv.com/ +Wood,Ohio,173,39,39173,county,Wood County,https://www.co.wood.oh.us/ +Wood,Wisconsin,141,55,55141,county,Wood County,https://www.woodcountywi.gov/ +Wood,Texas,499,48,48499,county,Wood County,https://www.mywoodcounty.com/ +Woodbury,Iowa,193,19,19193,county,Woodbury County,https://www.woodburycountyiowa.gov/ +Woodford,Kentucky,239,21,21239,county,Woodford County,https://woodfordcounty.ky.gov/ +Woodford,Illinois,203,17,17203,county,Woodford County,https://www.woodford-county.org/ +Woodruff,Arkansas,147,5,5147,county,Woodruff County,https://local.arkansas.gov/local.php?agency=Woodruff%20County +Woods,Oklahoma,151,40,40151,county,Woods County,https://oklahoma.gov/health/locations/county-health-departments/woods-county-health-department.html +Woodson,Kansas,207,20,20207,county,Woodson County,http://www.woodsoncounty.net/ +Woodward,Oklahoma,153,40,40153,county,Woodward County,https://woodward.okcounties.org/ +Worcester,Maryland,47,24,24047,county,Worcester County,https://www.co.worcester.md.us/ +Worcester,Massachusetts,27,25,25027,county,Worcester County,https://www.worcesterma.gov/ +Worth,Iowa,195,19,19195,county,Worth County,https://worthcountyiowa.gov/ +Worth,Missouri,227,29,29227,county,Worth County,http://worthcounty.us/ +Worth,Georgia,321,13,13321,county,Worth County,https://www.worthcountyboc.com/home +Wright,Missouri,229,29,29229,county,Wright County,https://www.wrightcountymo.gov/ +Wright,Minnesota,171,27,27171,county,Wright County,https://www.co.wright.mn.us/ +Wright,Iowa,197,19,19197,county,Wright County,https://www.wrightcounty.iowa.gov/ +Wyandot,Ohio,175,39,39175,county,Wyandot County,https://www.co.wyandot.oh.us/ +Wyandotte,Kansas,209,20,20209,county,Wyandotte County,https://www.wycokck.org/Home +Wyoming,West Virginia,109,54,54109,county,Wyoming County,https://www.wv.gov/local/Pages/counties.aspx?county=wyoming +Wyoming,New York,121,36,36121,county,Wyoming County,https://www.wyomingco.net/ +Wyoming,Pennsylvania,131,42,42131,county,Wyoming County,https://wyomingcountypa.gov/ +Wythe,Virginia,197,51,51197,county,Wythe County,http://www.wytheco.org/ +Yadkin,North Carolina,197,37,37197,county,Yadkin County,https://www.yadkincountync.gov/ +Yakima,Washington,77,53,53077,county,Yakima County,https://www.yakimacounty.us/ +Yalobusha,Mississippi,161,28,28161,county,Yalobusha County,http://www.yalobushaonline.org/ +Yamhill,Oregon,71,41,41071,county,Yamhill County,https://www.co.yamhill.or.us/ +Yancey,North Carolina,199,37,37199,county,Yancey County,https://yanceycountync.gov/ +Yankton,South Dakota,135,46,46135,county,Yankton County,http://www.co.yankton.sd.us/ +Yates,New York,123,36,36123,county,Yates County,https://www.yatescounty.org/ +Yavapai,Arizona,25,4,4025,county,Yavapai County,https://www.yavapaiaz.gov/Home +Yazoo,Mississippi,163,28,28163,county,Yazoo County,https://www.yazoocounty.net/ +Yell,Arkansas,149,5,5149,county,Yell County,https://yellcountyar.gov/ +Yellow Medicine,Minnesota,173,27,27173,county,Yellow Medicine County,https://www.co.ym.mn.gov/ +Yellowstone,Montana,111,30,30111,county,Yellowstone County,https://www.yellowstonecountymt.gov/ +Yoakum,Texas,501,48,48501,county,Yoakum County,https://www.co.yoakum.tx.us/ +Yolo,California,113,6,6113,county,Yolo County,https://www.yolocounty.org/ +York,South Carolina,91,45,45091,county,York County,https://www.yorkcountygov.com/ +York,Maine,31,23,23031,county,York County,https://www.yorkcountymaine.gov/ +York,Virginia,199,51,51199,county,York County,https://www.yorkcounty.gov/ +York,Pennsylvania,133,42,42133,county,York County,https://yorkcountypa.gov/ +York,Nebraska,185,31,31185,county,York County,https://www.yorkcounty.ne.gov/ +Young,Texas,503,48,48503,county,Young County,https://www.co.young.tx.us/ +Yuba,California,115,6,6115,county,Yuba County,https://www.yuba.org/ +Yuma,Colorado,125,8,8125,county,Yuma County,https://yumacounty.net/home/ +Yuma,Arizona,27,4,4027,county,Yuma County,https://www.yumacountyaz.gov/ +Zapata,Texas,505,48,48505,county,Zapata County,https://www.co.zapata.tx.us/ +Zavala,Texas,507,48,48507,county,Zavala County,https://www.co.zavala.tx.us/ +Ziebach,South Dakota,137,46,46137,county,Ziebach County,https://ziebachcounty.org/ diff --git a/elm/ords/download.py b/elm/ords/download.py new file mode 100644 index 00000000..dc4fa02c --- /dev/null +++ b/elm/ords/download.py @@ -0,0 +1,209 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance county file downloading logic""" +import pprint +import asyncio +import logging +from itertools import zip_longest, chain +from contextlib import AsyncExitStack + +from elm.ords.llm import StructuredLLMCaller +from elm.ords.extraction import check_for_ordinance_info +from elm.ords.services.threaded import TempFileCache +from elm.ords.validation.location import CountyValidator +from elm.web.document import PDFDocument +from elm.web.file_loader import AsyncFileLoader +from elm.web.google_search import PlaywrightGoogleLinkSearch + + +logger = logging.getLogger(__name__) +QUESTION_TEMPLATES = [ + '0. "wind energy conversion system zoning ordinances {location}"', + '1. "{location} wind WECS zoning ordinance"', + '2. "Where can I find the legal text for commercial wind energy ' + 'conversion system zoning ordinances in {location}?"', + '3. "What is the specific legal information regarding zoning ' + 'ordinances for commercial wind energy conversion systems in {location}?"', +] + + +async def _search_single( + location, question, browser_sem, num_results=10, **kwargs +): + """Perform a single google search.""" + if browser_sem is None: + browser_sem = AsyncExitStack() + + search_engine = PlaywrightGoogleLinkSearch(**kwargs) + async with browser_sem: + return await search_engine.results( + question.format(location=location), + num_results=num_results, + ) + + +async def _find_urls(location, num_results=10, browser_sem=None, **kwargs): + """Parse google search output for URLs.""" + searchers = [ + asyncio.create_task( + _search_single( + location, q, browser_sem, num_results=num_results, **kwargs + ), + name=location, + ) + for q in QUESTION_TEMPLATES + ] + return await asyncio.gather(*searchers) + + +def _down_select_urls(search_results, num_urls=5): + """Select the top 5 URLs.""" + all_urls = chain.from_iterable( + zip_longest(*[results[0] for results in search_results]) + ) + urls = set() + for url in all_urls: + if not url: + continue + urls.add(url) + if len(urls) == num_urls: + break + return urls + + +async def _load_docs(urls, text_splitter, browser_semaphore=None, **kwargs): + """Load a document for each input URL.""" + loader_kwargs = { + "html_read_kwargs": {"text_splitter": text_splitter}, + "file_cache_coroutine": TempFileCache.call, + "browser_semaphore": browser_semaphore, + } + loader_kwargs.update(kwargs) + file_loader = AsyncFileLoader(**loader_kwargs) + docs = await file_loader.fetch_all(*urls) + + logger.debug( + "Loaded the following number of pages for docs: %s", + pprint.PrettyPrinter().pformat( + { + doc.metadata.get("source", "Unknown"): len(doc.pages) + for doc in docs + } + ), + ) + return [doc for doc in docs if not doc.empty] + + +async def _down_select_docs_correct_location( + docs, location, county, state, **kwargs +): + """Remove all documents not pertaining to the location.""" + llm_caller = StructuredLLMCaller(**kwargs) + county_validator = CountyValidator(llm_caller) + searchers = [ + asyncio.create_task( + county_validator.check(doc, county=county, state=state), + name=location, + ) + for doc in docs + ] + output = await asyncio.gather(*searchers) + correct_loc_docs = [doc for doc, check in zip(docs, output) if check] + return sorted( + correct_loc_docs, + key=lambda doc: (not isinstance(doc, PDFDocument), len(doc.text)), + ) + + +async def _check_docs_for_ords(docs, text_splitter, **kwargs): + """Check documents to see if they contain ordinance info.""" + ord_docs = [] + for doc in docs: + doc = await check_for_ordinance_info(doc, text_splitter, **kwargs) + if doc.metadata["contains_ord_info"]: + ord_docs.append(doc) + return ord_docs + + +def _parse_all_ord_docs(all_ord_docs): + """Parse a list of documents and get the result for the best match.""" + if not all_ord_docs: + return None + + return sorted(all_ord_docs, key=_ord_doc_sorting_key)[-1] + + +def _ord_doc_sorting_key(doc): + """All text sorting key""" + year, month, day = doc.metadata.get("date", (-1, -1, -1)) + return year, isinstance(doc, PDFDocument), -1 * len(doc.text), month, day + + +async def download_county_ordinance( + location, + text_splitter, + num_urls=5, + file_loader_kwargs=None, + browser_semaphore=None, + **kwargs +): + """Download the ordinance document for a single county. + + Parameters + ---------- + location : elm.ords.utilities.location.Location + Location objects representing the county. + text_splitter : obj, optional + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. Langchain's text splitters should work for this + input. + num_urls : int, optional + Number of unique Google search result URL's to check for + ordinance document. By default, ``5``. + file_loader_kwargs : dict, optional + Dictionary of keyword-argument pairs to initialize + :class:`elm.web.file_loader.AsyncFileLoader` with. The + "pw_launch_kwargs" key in these will also be used to initialize + the :class:`elm.web.google_search.PlaywrightGoogleLinkSearch` + used for the google URL search. By default, ``None``. + browser_semaphore : asyncio.Semaphore, optional + Semaphore instance that can be used to limit the number of + playwright browsers open concurrently. If ``None``, no limits + are applied. By default, ``None``. + **kwargs + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. + + Returns + ------- + elm.web.document.BaseDocument | None + Document instance for the downloaded document, or ``None`` if no + document was found. + """ + file_loader_kwargs = file_loader_kwargs or {} + pw_launch_kwargs = file_loader_kwargs.get("pw_launch_kwargs", {}) + urls = await _find_urls( + location.full_name, + num_results=10, + browser_sem=browser_semaphore, + **pw_launch_kwargs + ) + urls = _down_select_urls(urls, num_urls=num_urls) + logger.debug("Downloading documents for URLS: \n\t-%s", "\n\t-".join(urls)) + docs = await _load_docs( + urls, text_splitter, browser_semaphore, **file_loader_kwargs + ) + docs = await _down_select_docs_correct_location( + docs, + location=location.full_name, + county=location.name, + state=location.state, + **kwargs + ) + docs = await _check_docs_for_ords(docs, text_splitter, **kwargs) + logger.info( + "Found %d potential ordinance documents for %s", + len(docs), + location.full_name, + ) + return _parse_all_ord_docs(docs) diff --git a/elm/ords/extraction/__init__.py b/elm/ords/extraction/__init__.py new file mode 100644 index 00000000..0495ef97 --- /dev/null +++ b/elm/ords/extraction/__init__.py @@ -0,0 +1,8 @@ +"""ELM Ordinance text extraction tooling. """ + +from .apply import ( + check_for_ordinance_info, + extract_ordinance_text_with_llm, + extract_ordinance_text_with_ngram_validation, + extract_ordinance_values, +) diff --git a/elm/ords/extraction/apply.py b/elm/ords/extraction/apply.py new file mode 100644 index 00000000..875f4847 --- /dev/null +++ b/elm/ords/extraction/apply.py @@ -0,0 +1,310 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance function to apply ordinance extraction on a document """ +import logging +from warnings import warn + +from elm.ords.llm import LLMCaller, StructuredLLMCaller +from elm.ords.extraction.date import DateExtractor +from elm.ords.extraction.ordinance import ( + OrdinanceValidator, + OrdinanceExtractor, +) +from elm.ords.extraction.parse import StructuredOrdinanceParser + + +logger = logging.getLogger(__name__) + + +async def check_for_ordinance_info(doc, text_splitter, **kwargs): + """Parse a single document for ordinance information. + + Parameters + ---------- + doc : elm.web.document.BaseDocument + A document potentially containing ordinance information. Note + that if the document's metadata contains the + ``"contains_ord_info"`` key, it will not be processed. To force + a document to be processed by this function, remove that key + from the documents metadata. + text_splitter : obj + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. Langchain's text splitters should work for this + input. + **kwargs + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. + + Returns + ------- + elm.web.document.BaseDocument + Document that has been parsed for ordinance text. The results of + the parsing are stored in the documents metadata. In particular, + the metadata will contain a ``"contains_ord_info"`` key that + will be set to ``True`` if ordinance info was found in the text, + and ``False`` otherwise. If ``True``, the metadata will also + contain a ``"date"`` key containing the most recent date that + the ordinance was enacted (or a tuple of `None` if not found), + and an ``"ordinance_text"`` key containing the ordinance text + snippet. Note that the snippet may contain other info as well, + but should encapsulate all of the ordinance text. + """ + if "contains_ord_info" in doc.metadata: + return doc + + llm_caller = StructuredLLMCaller(**kwargs) + chunks = text_splitter.split_text(doc.text) + validator = OrdinanceValidator(llm_caller, chunks) + doc.metadata["contains_ord_info"] = await validator.parse() + if doc.metadata["contains_ord_info"]: + doc.metadata["date"] = await DateExtractor(llm_caller).parse(doc) + doc.metadata["ordinance_text"] = validator.ordinance_text + + return doc + + +async def extract_ordinance_text_with_llm(doc, text_splitter, extractor): + """Extract ordinance text from document using LLM. + + Parameters + ---------- + doc : elm.web.document.BaseDocument + A document known to contain ordinance information. This means it + must contain an ``"ordinance_text"`` key in the metadata. You + can run + :func:`~elm.ords.extraction.apply.check_for_ordinance_info` + to have this attribute populated automatically for documents + that are found to contain ordinance data. Note that if the + document's metadata does not contain the ``"ordinance_text"`` + key, you will get an error. + text_splitter : obj + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. Langchain's text splitters should work for this + input. + extractor : elm.ords.extraction.ordinance.OrdinanceExtractor + Instance of `~elm.ords.extraction.ordinance.OrdinanceExtractor` + used for ordinance text extraction. + + Returns + ------- + elm.web.document.BaseDocument + Document that has been parsed for ordinance text. The results of + the extraction are stored in the document's metadata. In + particular, the metadata will contain a + ``"cleaned_ordinance_text"`` key that will contain the cleaned + ordinance text. + """ + text_chunks = text_splitter.split_text(doc.metadata["ordinance_text"]) + ordinance_text = await extractor.check_for_restrictions(text_chunks) + doc.metadata["restrictions_ordinance_text"] = ordinance_text + + text_chunks = text_splitter.split_text(ordinance_text) + ordinance_text = await extractor.check_for_correct_size(text_chunks) + doc.metadata["cleaned_ordinance_text"] = ordinance_text + + return doc + + +async def extract_ordinance_text_with_ngram_validation( + doc, + text_splitter, + n=4, + num_extraction_attempts=3, + ngram_fraction_threshold=0.95, + **kwargs, +): + """Extract ordinance text for a single document with known ord info. + + This extraction includes an "ngram" check, which attempts to detect + wether or not the cleaned text was extracted from the original + ordinance text. The processing will attempt to re-extract the text + if the validation does not pass a certain threshold until the + maximum number of attempts is reached. If the text still does not + pass validation at this point, there is a good chance that the LLM + hallucinated parts of the output text, so caution should be taken. + + Parameters + ---------- + doc : elm.web.document.BaseDocument + A document known to contain ordinance information. This means it + must contain an ``"ordinance_text"`` key in the metadata. You + can run + :func:`~elm.ords.extraction.apply.check_for_ordinance_info` + to have this attribute populated automatically for documents + that are found to contain ordinance data. Note that if the + document's metadata does not contain the ``"ordinance_text"`` + key, it will not be processed. + text_splitter : obj + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. Langchain's text splitters should work for this + input. + n : int, optional + Number of words to include per ngram for the ngram validation, + which helps ensure that the LLM did not hallucinate. + By default, ``4``. + num_extraction_attempts : int, optional + Number of extraction attempts before returning text that did not + pass the ngram check. If the processing exceeds this value, + there is a good chance that the LLM hallucinated parts of the + output text. Cannot be negative or 0. By default, ``3``. + ngram_fraction_threshold : float, optional + Fraction of ngrams in the cleaned text that are also found in + the original ordinance text for the extraction to be considered + successful. Should be a value between 0 and 1 (inclusive). + By default, ``0.95``. + **kwargs + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. + + Returns + ------- + elm.web.document.BaseDocument + Document that has been parsed for ordinance text. The results of + the extraction are stored in the document's metadata. In + particular, the metadata will contain a + ``"cleaned_ordinance_text"`` key that will contain the cleaned + ordinance text. + """ + if not doc.metadata.get("ordinance_text"): + msg = ( + "Input document has no 'ordinance_text' key or string does not " + "contain information. Please run `check_for_ordinance_info` " + "prior to calling this method." + ) + logger.warning(msg) + warn(msg, UserWarning) + return doc + + llm_caller = LLMCaller(**kwargs) + extractor = OrdinanceExtractor(llm_caller) + + doc = await _extract_with_ngram_check( + doc, + text_splitter, + extractor, + n=max(1, n), + num_tries=max(1, num_extraction_attempts), + ngram_fraction_threshold=max(0, min(1, ngram_fraction_threshold)), + ) + + return doc + + +async def _extract_with_ngram_check( + doc, + text_splitter, + extractor, + n=4, + num_tries=3, + ngram_fraction_threshold=0.95, +): + """Extract ordinance info from doc and validate using ngrams.""" + from elm.ords.extraction.ngrams import sentence_ngram_containment + + source = doc.metadata.get("source", "Unknown") + og_text = doc.metadata["ordinance_text"] + if not og_text: + msg = ( + "Document missing original ordinance text! No extraction " + "performed (Document source: %s)", + source, + ) + logger.warning(msg) + warn(msg, UserWarning) + return doc + + best_score = 0 + best_summary = "" + for attempt in range(num_tries): + doc = await extract_ordinance_text_with_llm( + doc, text_splitter, extractor + ) + cleaned_text = doc.metadata["cleaned_ordinance_text"] + if not cleaned_text: + logger.debug( + "No cleaned text found after extraction on attempt %d " + "for document with source %s. Retrying...", + attempt, + source, + ) + continue + + ngram_frac = sentence_ngram_containment( + original=og_text, test=cleaned_text, n=n + ) + if ngram_frac >= ngram_fraction_threshold: + logger.debug( + "Document extraction passed ngram check on attempt %d " + "with score %.2f (Document source: %s)", + attempt + 1, + ngram_frac, + source, + ) + break + + if ngram_frac > best_score: + best_score = ngram_frac + best_summary = cleaned_text + + logger.debug( + "Document extraction failed ngram check on attempt %d " + "with score %.2f (Document source: %s). Retrying...", + attempt + 1, + ngram_frac, + source, + ) + else: + doc.metadata["cleaned_ordinance_text"] = best_summary + msg = ( + f"Ngram check failed after {num_tries}. LLM hallucination in " + "cleaned ordinance text is extremely likely! Proceed with " + f"caution!! (Document source: {best_score})" + ) + logger.warning(msg) + warn(msg, UserWarning) + + return doc + + +async def extract_ordinance_values(doc, **kwargs): + """Extract ordinance values for a single document with known ord text. + + Parameters + ---------- + doc : elm.web.document.BaseDocument + A document known to contain ordinance text. This means it must + contain an ``"cleaned_ordinance_text"`` key in the metadata. You + can run + :func:`~elm.ords.extraction.apply.extract_ordinance_text` + to have this attribute populated automatically for documents + that are found to contain ordinance data. Note that if the + document's metadata does not contain the + ``"cleaned_ordinance_text"`` key, it will not be processed. + **kwargs + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. + + Returns + ------- + elm.web.document.BaseDocument + Document that has been parsed for ordinance values. The results + of the extraction are stored in the document's metadata. In + particular, the metadata will contain an ``"ordinance_values"`` + key that will contain the DataFame with ordinance values. + """ + if not doc.metadata.get("cleaned_ordinance_text"): + msg = ( + "Input document has no 'cleaned_ordinance_text' key or string " + "does not contain info. Please run `extract_ordinance_text` " + "prior to calling this method." + ) + logger.warning(msg) + warn(msg, UserWarning) + return doc + + parser = StructuredOrdinanceParser(**kwargs) + text = doc.metadata["cleaned_ordinance_text"] + doc.metadata["ordinance_values"] = await parser.parse(text) + return doc diff --git a/elm/ords/extraction/date.py b/elm/ords/extraction/date.py new file mode 100644 index 00000000..c6e53c44 --- /dev/null +++ b/elm/ords/extraction/date.py @@ -0,0 +1,105 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance date extraction logic.""" +import logging + +logger = logging.getLogger(__name__) + + +class DateExtractor: + """Helper class to extract date info from document.""" + + SYSTEM_MESSAGE = ( + "You are a legal scholar that reads ordinance text and extracts " + "structured date information. Return your answer in JSON format (not " + "markdown). Your JSON file must include exactly four keys. The first " + "key is 'explanation', which contains a short summary of the most " + "relevant date information you found in the text. The second key is " + "'year', which should contain an integer value that represents the " + "latest year this ordinance was enacted/updated, or null if that " + "information cannot be found in the text. The third key is 'month', " + "which should contain an integer value that represents the latest " + "month of the year this ordinance was enacted/updated, or null if " + "that information cannot be found in the text. The fourth key is " + "'day', which should contain an integer value that represents the " + "latest day of the month this ordinance was enacted/updated, or null " + "if that information cannot be found in the text." + ) + + def __init__(self, structured_llm_caller): + """ + + Parameters + ---------- + structured_llm_caller : elm.ords.llm.StructuredLLMCaller + StructuredLLMCaller instance. Used for structured validation + queries. + """ + self.slc = structured_llm_caller + + async def parse(self, doc): + """Extract date (year, month, day) from doc. + + Parameters + ---------- + doc : elm.web.document.BaseDocument + Document with a `raw_pages` attribute. + + Returns + ------- + tuple + 3-tuple containing year, month, day, or ``None`` if any of + those are not found. + """ + all_years = [] + if not doc.raw_pages: + return None, None, None + + for text in doc.raw_pages: + if not text: + continue + + response = await self.slc.call( + sys_msg=self.SYSTEM_MESSAGE, + content=f"Please extract the date for this ordinance:\n{text}", + usage_sub_label="date_extraction", + ) + if not response: + continue + all_years.append(response) + + return _parse_date(all_years) + + +def _parse_date(json_list): + """Parse all date elements.""" + year = _parse_date_element( + json_list, + key="year", + max_len=4, + min_val=2000, + max_val=float("inf"), + ) + month = _parse_date_element( + json_list, key="month", max_len=2, min_val=1, max_val=12 + ) + day = _parse_date_element( + json_list, key="day", max_len=2, min_val=1, max_val=31 + ) + + return year, month, day + + +def _parse_date_element(json_list, key, max_len, min_val, max_val): + """Parse out a single date element.""" + date_elements = [info.get(key) for info in json_list] + logger.debug(f"{key=}, {date_elements=}") + date_elements = [ + int(y) + for y in date_elements + if y is not None + and len(str(y)) <= max_len + and (min_val <= int(y) <= max_val) + ] + if not date_elements: + return -1 * float("inf") + return max(date_elements) diff --git a/elm/ords/extraction/features.py b/elm/ords/extraction/features.py new file mode 100644 index 00000000..f40a029a --- /dev/null +++ b/elm/ords/extraction/features.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance mutually-exclusive features class.""" + + +class SetbackFeatures: + """Utility class to get mutually-exclusive feature descriptions.""" + + DEFAULT_FEATURE_DESCRIPTIONS = { + "struct": [ + "occupied dwellings", + "buildings", + "structures", + "residences", + ], + "pline": ["property lines", "parcels", "subdivisions"], + "roads": ["roads"], # , "rights-of-way"], + "rail": ["railroads"], + "trans": [ + "overhead electrical transmission lines", + "overhead utility lines", + "utility easements", + "utility lines", + "power lines", + "electrical lines", + "transmission lines", + ], + "water": ["lakes", "reservoirs", "streams", "rivers", "wetlands"], + } + FEATURES_AS_IGNORE = { + "struct": "structures", + "pline": "property lines", + "roads": "roads", + "rail": "railroads", + "trans": "transmission lines", + "water": "wetlands", + } + FEATURE_CLARIFICATIONS = { + "struct": "", + "pline": "", + "roads": "Roads may also be labeled as rights-of-way. ", + "rail": "", + "trans": "", + "water": "", + } + + def __init__(self): + self._validate_descriptions() + + def __iter__(self): + for feature_id in self.DEFAULT_FEATURE_DESCRIPTIONS: + feature, ignore = self._keep_and_ignore(feature_id) + clarification = self.FEATURE_CLARIFICATIONS.get(feature_id, "") + yield { + "feature_id": feature_id, + "feature": feature, + "ignore_features": ignore, + "feature_clarifications": clarification, + } + + def _validate_descriptions(self): + """Ensure all features have at least one description.""" + features_missing_descriptors = set() + for feature, descriptions in self.DEFAULT_FEATURE_DESCRIPTIONS.items(): + if len(descriptions) < 1: + features_missing_descriptors.add(feature) + + if any(features_missing_descriptors): + raise ValueError( + f"The following features are missing descriptors: " + f"{features_missing_descriptors}" + ) + + def _keep_and_ignore(self, feature_id): + """Get the keep and ignore phrases for a feature.""" + keep_keywords = self.DEFAULT_FEATURE_DESCRIPTIONS[feature_id] + ignore = [ + keyword + for feat_id, keyword in self.FEATURES_AS_IGNORE.items() + if feat_id != feature_id + ] + + keep_phrase = _join_keywords(keep_keywords, final_sep=", and/or ") + ignore_phrase = _join_keywords(ignore, final_sep=", and ") + + return keep_phrase, ignore_phrase + + +def _join_keywords(keywords, final_sep): + """Join a list of keywords/descriptions.""" + if len(keywords) < 1: + return "" + + if len(keywords) == 1: + return keywords[0] + + comma_separated = ", ".join(keywords[:-1]) + return final_sep.join([comma_separated, keywords[-1]]) diff --git a/elm/ords/extraction/graphs.py b/elm/ords/extraction/graphs.py new file mode 100644 index 00000000..16cfa1b8 --- /dev/null +++ b/elm/ords/extraction/graphs.py @@ -0,0 +1,484 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance Decision Tree Graph setup functions.""" +import networkx as nx + + +_SECTION_PROMPT = ( + 'The value of the "section" key should be a string representing the ' + "title of the section (including numerical labels), if it's given, " + "and `null` otherwise." +) +_COMMENT_PROMPT = ( + 'The value of the "comment" key should be a one-sentence explanation ' + "of how you determined the value, if you think it is necessary " + "(`null` otherwise)." +) +EXTRACT_ORIGINAL_TEXT_PROMPT = ( + "Can you extract the raw text with original formatting " + "that states how close I can site {wes_type} to {feature}? " +) + + +def _setup_graph_no_nodes(**kwargs): + return nx.DiGraph( + SECTION_PROMPT=_SECTION_PROMPT, + COMMENT_PROMPT=_COMMENT_PROMPT, + **kwargs + ) + + +def llm_response_starts_with_yes(response): + """Check if LLM response begins with "yes" (case-insensitive) + + Parameters + ---------- + response : str + LLM response string. + + Returns + ------- + bool + `True` if LLM response begins with "Yes". + """ + return response.lower().startswith("yes") + + +def llm_response_starts_with_no(response): + """Check if LLM response begins with "no" (case-insensitive) + + Parameters + ---------- + response : str + LLM response string. + + Returns + ------- + bool + `True` if LLM response begins with "No". + """ + return response.lower().startswith("no") + + +def llm_response_does_not_start_with_no(response): + """Check if LLM response does not start with "no" (case-insensitive) + + Parameters + ---------- + response : str + LLM response string. + + Returns + ------- + bool + `True` if LLM response does not begin with "No". + """ + return not llm_response_starts_with_no(response) + + +def setup_graph_wes_types(**kwargs): + """Setup Graph to get the largest turbine size in the ordinance text. + + Parameters + ---------- + **kwargs + Keyword-value pairs to add to graph. + + Returns + ------- + nx.DiGraph + Graph instance that can be used to initialize an + `elm.tree.DecisionTree`. + """ + G = _setup_graph_no_nodes(**kwargs) + + G.add_node( + "init", + prompt=( + "Does the following text distinguish between multiple " + "turbine sizes? Distinctions are often made as 'small' vs 'large' " + "wind energy conversion systems or actual MW values. " + "Begin your response with either 'Yes' or 'No' and explain your " + "answer." + '\n\n"""\n{text}\n"""' + ), + ) + + G.add_edge("init", "get_text", condition=llm_response_starts_with_yes) + G.add_node( + "get_text", + prompt=( + "What are the different turbine sizes this text mentions? " + "List them in order of increasing size." + ), + ) + G.add_edge("get_text", "final") + G.add_node( + "final", + prompt=( + "Respond based on our entire conversation so far. Return your " + "answer in JSON format (not markdown). Your JSON file must " + 'include exactly two keys. The keys are "largest_wes_type" and ' + '"explanation". The value of the "largest_wes_type" key should ' + "be a string that labels the largest wind energy conversion " + 'system mentioned in the text. The value of the "explanation" ' + "key should be a string containing a short explanation for your " + "choice." + ), + ) + return G + + +def setup_base_graph(**kwargs): + """Setup Graph to get setback ordinance text for a particular feature. + + Parameters + ---------- + **kwargs + Keyword-value pairs to add to graph. + + Returns + ------- + nx.DiGraph + Graph instance that can be used to initialize an + `elm.tree.DecisionTree`. + """ + G = _setup_graph_no_nodes(**kwargs) + + G.add_node( + "init", + prompt=( + "Is there text in the following legal document that describes " + "how close I can site or how far I have to setback " + "{wes_type} to {feature}? {feature_clarifications}" + "Pay extra attention to clarifying text found in parentheses " + "and footnotes. Begin your response with either 'Yes' or 'No' " + "and explain your answer." + '\n\n"""\n{text}\n"""' + ), + ) + + G.add_edge( + "init", "get_text", condition=llm_response_does_not_start_with_no + ) + G.add_node("get_text", prompt=EXTRACT_ORIGINAL_TEXT_PROMPT) + + return G + + +def setup_participating_owner(**kwargs): + """Setup Graph to check for participating vs non-participating owner + setbacks for a feature. + + Parameters + ---------- + **kwargs + Keyword-value pairs to add to graph. + + Returns + ------- + nx.DiGraph + Graph instance that can be used to initialize an + `elm.tree.DecisionTree`. + """ + G = _setup_graph_no_nodes(**kwargs) + + G.add_node( + "init", + prompt=( + "Does the ordinance for {feature} setbacks explicitly specify " + "a value that applies to participating owners? Occupying owners " + "are not participating owners unless explicitly mentioned in the " + "text. Justify your answer by quoting the raw text directly." + ), + ) + G.add_edge("init", "non_part") + G.add_node( + "non_part", + prompt=( + "Does the ordinance for {feature} setbacks explicitly specify " + "a value that applies to non-participating owners? Non-occupying " + "owners are not non-participating owners unless explicitly " + "mentioned in the text. Justify your answer by quoting the raw " + "text directly." + ), + ) + G.add_edge("non_part", "final") + G.add_node( + "final", + prompt=( + "Now we are ready to extract structured data. Respond based on " + "our entire conversation so far. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly two " + 'keys. The keys are "participating" and "non-participating". The ' + 'value of the "participating" key should be a string containing ' + "the raw text with original formatting from the ordinance that " + "applies to participating owners or `null` if there was no such " + 'text. The value of the "non-participating" key should be a ' + "string containing the raw text with original formatting from the " + "ordinance that applies to non-participating owners or simply the " + "full ordinance if the text did not make the distinction between " + "participating and non-participating owners." + ), + ) + return G + + +def setup_multiplier(**kwargs): + """Setup Graph to extract a setbacks multiplier values for a feature. + + Parameters + ---------- + **kwargs + Keyword-value pairs to add to graph. + + Returns + ------- + nx.DiGraph + Graph instance that can be used to initialize an + `elm.tree.DecisionTree`. + """ + G = _setup_graph_no_nodes(**kwargs) + + G.add_node( + "init", + prompt=( + "We will attempt to extract structured data for this ordinance. " + "Let's think step by step. Does the text mention a multiplier " + "that should be applied to a turbine dimension (e.g. height, " + "rotor diameter, etc) to compute the setback distance from " + "{feature}? Ignore any text related to {ignore_features}. " + "Remember that 1 is a valid multiplier, and treat any mention of " + "'fall zone' as a system height multiplier of 1. Begin your " + "response with either 'Yes' or 'No' and explain your answer." + ), + ) + G.add_edge("init", "no_multiplier", condition=llm_response_starts_with_no) + G.add_node( + "no_multiplier", + prompt=( + "Does the ordinance give the setback from {feature} as a fixed " + "distance value? Explain yourself." + ), + ) + G.add_edge("no_multiplier", "out_static") + G.add_node( + "out_static", + prompt=( + "Now we are ready to extract structured data. Respond based on " + "our entire conversation so far. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly " + 'four keys. The keys are "fixed_value", "units", "section", ' + '"comment". The value of the "fixed_value" key should be a ' + "numerical value corresponding to the setback distance value " + "from {feature} or `null` if there was no such value. The value " + 'of the "units" key should be a string corresponding to the units ' + "of the setback distance value from {feature} or `null` if there " + 'was no such value. {SECTION_PROMPT} The value of the "comment" ' + "key should be a one-sentence explanation of how you determined " + "the value, or a short description of the ordinance itself if no " + "multiplier or static setback value was found." + ), + ) + G.add_edge("init", "mult_single", condition=llm_response_starts_with_yes) + + G.add_node( + "mult_single", + prompt=( + "Are multiple values given for the multiplier used to " + "compute the setback distance value from {feature}? If so, " + "select and state the largest one. Otherwise, repeat the single " + "multiplier value that was given in the text. " + ), + ) + G.add_edge("mult_single", "mult_type") + G.add_node( + "mult_type", + prompt=( + "What should the multiplier be applied to? Common acronyms " + "include RD for rotor diameter and HH for hub height. Remember " + "that system/total height is the tip-hight of the turbine. " + "Select a value from the following list and explain yourself: " + "['tip-height-multiplier', 'hub-height-multiplier', " + "'rotor-diameter-multiplier]" + ), + ) + + G.add_edge("mult_type", "adder") + G.add_node( + "adder", + prompt=( + "Does the ordinance include a static distance value that " + "should be added to the result of the multiplication? Do not " + "confuse this value with static setback requirements. Ignore text " + "with clauses such as 'no lesser than', 'no greater than', " + "'the lesser of', or 'the greater of'. Begin your response with " + "either 'Yes' or 'No' and explain your answer, stating the adder " + "value if it exists." + ), + ) + G.add_edge("adder", "out_mult", condition=llm_response_starts_with_no) + G.add_edge("adder", "adder_eq", condition=llm_response_starts_with_yes) + + G.add_node( + "adder_eq", + prompt=( + "We are only interested in adders that satisfy the following " + "equation: 'multiplier * turbine_dimension + '. Does the " + "adder value you identified satisfy this equation? Begin your " + "response with either 'Yes' or 'No' and explain your answer." + ), + ) + G.add_edge("adder_eq", "out_mult", condition=llm_response_starts_with_no) + G.add_edge( + "adder_eq", + "conversion", + condition=llm_response_starts_with_yes, + ) + G.add_node( + "conversion", + prompt=( + "If the adder value is not given in feet, convert " + "it to feet (remember that there are 3.28084 feet in one meter " + "and 5280 feet in one mile). Show your work step-by-step " + "if you had to perform a conversion." + ), + ) + G.add_edge("conversion", "out_mult") + + G.add_node( + "out_mult", + prompt=( + "Now we are ready to extract structured data. Respond based on " + "our entire conversation so far. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly five " + 'keys. The keys are "mult_value", "mult_type", "adder", ' + '"section", "comment". The value of the "mult_value" key should ' + "be a numerical value corresponding to the multiplier value we " + 'determined earlier. The value of the "mult_type" key should be ' + "a string corresponding to the dimension that the multiplier " + "should be applied to, as we determined earlier. The value of " + 'the "adder" key should be a numerical value corresponding to ' + "the static value to be added to the total setback distance after " + "multiplication, as we determined earlier, or `null` if there is " + "no such value. {SECTION_PROMPT} {COMMENT_PROMPT}" + ), + ) + + return G + + +def setup_conditional(**kwargs): + """Setup Graph to extract min/max setback values (after mult) for a + feature. These are typically given within the context of + 'the greater of' or 'the lesser of' clauses. + + Parameters + ---------- + **kwargs + Keyword-value pairs to add to graph. + + Returns + ------- + nx.DiGraph + Graph instance that can be used to initialize an + `elm.tree.DecisionTree`. + """ + G = _setup_graph_no_nodes(**kwargs) + + G.add_node( + "init", + prompt=( + "We will attempt to extract structured data for this ordinance. " + "Let's think step by step. Does the setback from {feature} " + "mention a minimum or maximum static setback distance regardless " + "of the outcome of the multiplier calculation? This is often " + "phrased as 'the greater of' or 'the lesser of'. Do not confuse " + "this value with static values to be added to multiplicative " + "setbacks. Begin your response with either 'Yes' or 'No' and " + "explain your answer." + ), + ) + + G.add_edge("init", "conversions", condition=llm_response_starts_with_yes) + G.add_node( + "conversions", + prompt=( + "Tell me the minimum and/or maximum setback distances, " + "converting to feet if necessary (remember that there are " + "3.28084 feet in one meter and 5280 feet in one mile). " + "Explain your answer and show your work if you had to perform " + "a conversion." + ), + ) + + G.add_edge("conversions", "out_condition") + G.add_node( + "out_condition", + prompt=( + "Now we are ready to extract structured data. Respond based " + "on our entire conversation so far. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly two " + 'keys. The keys are "min_dist" and "max_dist". The value of the ' + '"min_dist" key should be a numerical value corresponding to the ' + "minimum setback value from {feature} we determined earlier, or " + '`null` if no such value exists. The value of the "max_dist" key ' + "should be a numerical value corresponding to the maximum setback " + "value from {feature} we determined earlier, or `null` if no such " + "value exists." + ), + ) + + return G + + +def setup_graph_extra_restriction(**kwargs): + """Setup Graph to extract non-setback ordinance values from text. + + Parameters + ---------- + **kwargs + Keyword-value pairs to add to graph. + + Returns + ------- + nx.DiGraph + Graph instance that can be used to initialize an + `elm.tree.DecisionTree`. + """ + G = _setup_graph_no_nodes(**kwargs) + + G.add_node( + "init", + prompt=( + "We will attempt to extract structured data for this " + "ordinance. Let's think step by step. Does the following text " + "explicitly limit the {restriction} allowed for {wes_type}? " + "Do not infer based on other restrictions; if this particular " + "restriction is not explicitly mentioned then say 'No'. Pay extra " + "attention to clarifying text found in parentheses and footnotes. " + "Begin your response with either 'Yes' or 'No' and explain " + "your answer." + '\n\n"""\n{text}\n"""' + ), + ) + G.add_edge("init", "final", condition=llm_response_starts_with_yes) + + G.add_node( + "final", + prompt=( + "Now we are ready to extract structured data. Respond based " + "on our entire conversation so far. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly four " + 'keys. The keys are "value", "units", "section", "comment". The ' + 'value of the "value" key should be a numerical value ' + "corresponding to the {restriction} allowed for {wes_type}, or " + "`null` if the text does not mention such a restriction. Use our " + 'conversation to fill out this value. The value of the "units" ' + "key should be a string corresponding to the units for the " + "{restriction} allowed for {wes_type} by the text below, or " + "`null` if the text does not mention such a restriction. Make " + 'sure to include any "per XXX" clauses in the units. ' + "{SECTION_PROMPT} {COMMENT_PROMPT}" + ), + ) + return G diff --git a/elm/ords/extraction/ngrams.py b/elm/ords/extraction/ngrams.py new file mode 100644 index 00000000..1e85adb4 --- /dev/null +++ b/elm/ords/extraction/ngrams.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance ngram text validation + +This check helps validate that the LLM extracted text from the original +document and did not make it up itself. +""" +import nltk +from nltk.tokenize import word_tokenize +from nltk.tokenize import sent_tokenize +from nltk.corpus import stopwords +from nltk.util import ngrams + + +nltk.download("punkt") +nltk.download("stopwords") +STOP_WORDS = set(stopwords.words("english")) +PUNCTUATIONS = {'"', ".", "(", ")", ",", "?", ";", ":", "''", "``"} + + +def _check_word(word): + """``True`` if a word is not a stop word or a punctuation.""" + return word not in STOP_WORDS and word not in PUNCTUATIONS + + +def _filtered_words(sentence): + """Filter out common words and punctuations.""" + return [ + word.casefold() + for word in word_tokenize(sentence) + if _check_word(word.casefold()) + ] + + +def convert_text_to_sentence_ngrams(text, n): + """Convert input text to a list of ngrams. + + The text is first split byu sentence, after which each sentence is + converted into ngrams. The ngrams for all sentences are combined and + returned. + + Parameters + ---------- + text : str + Input text containing one or more sentences. + n : int + Number of words to include per ngram. + + Returns + ------- + list + List of tuples, where each tuple is an ngram from the original + text. + """ + all_ngrams = [] + sentences = sent_tokenize(text) + for sentence in sentences: + words = _filtered_words(sentence) + all_ngrams += list(ngrams(words, n)) + return all_ngrams + + +def sentence_ngram_containment(original, test, n): + """Fraction of sentence ngrams from the test text found in the original. + + Parameters + ---------- + original : str + Original (superset) text. Ngrams from the `test` text will be + checked against this text. + test : str + Test (sub) text. Ngrams from this text will be searched for in + the original text, and the fraction of these ngrams that are + found in the original text will be returned. + n : int + Number of words to include per ngram. + + Returns + ------- + float + Fraction of ngrams from the `test` input that were found in the + `original` text. Always returns ``True`` if test has no ngrams. + """ + ngrams_test = convert_text_to_sentence_ngrams(test, n) + num_test_ngrams = len(ngrams_test) + if not num_test_ngrams: + return True + + ngrams_original = set(convert_text_to_sentence_ngrams(original, n)) + num_ngrams_found = sum(t in ngrams_original for t in ngrams_test) + return num_ngrams_found / num_test_ngrams diff --git a/elm/ords/extraction/ordinance.py b/elm/ords/extraction/ordinance.py new file mode 100644 index 00000000..a77e8320 --- /dev/null +++ b/elm/ords/extraction/ordinance.py @@ -0,0 +1,331 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance document content Validation logic + +These are primarily used to validate that a legal document applies to a +particular technology (e.g. Large Wind Energy Conversion Systems). +""" +import asyncio +import logging + +from elm import ApiBase +from elm.ords.validation.content import ( + ValidationWithMemory, + possibly_mentions_wind, +) +from elm.ords.utilities.parsing import merge_overlapping_texts + + +logger = logging.getLogger(__name__) + + +RESTRICTIONS = """- buildings / structures / residences +- property lines / parcels / subdivisions +- roads / rights-of-way +- railroads +- overhead electrical transmission wires +- bodies of water including wetlands, lakes, reservoirs, streams, and rivers +- natural, wildlife, and environmental conservation areas +- noise restrictions +- shadow flicker restrictions +- density restrictions +- turbine height restrictions +- minimum/maximum lot size +""" + + +class OrdinanceValidator(ValidationWithMemory): + """Check document text for wind ordinances.""" + + IS_LEGAL_TEXT_PROMPT = ( + "You extract structured data from text. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly three " + "keys. The first key is 'summary', which is a string that provides a " + "short summary of the text. The second key is 'type', which is a " + "string that best represent the type of document this text belongs " + "to. The third key is '{key}', which is a boolean that is set to " + "True if the type of the text (as you previously determined) is a " + "legally-binding statute or code and False if the text is an excerpt " + "from other non-legal text such as a news article, survey, summary, " + "application, public notice, etc." + ) + + CONTAINS_ORD_PROMPT = ( + "You extract structured data from text. Return your answer in JSON " + "format (not markdown). Your JSON file must include exactly three " + "keys. The first key is 'wind_reqs', which is a string that " + "summarizes the setbacks or other geospatial siting requirements (if " + "any) given in the text for a wind turbine. The second key is 'reqs', " + "which lists the quantitative values from the text excerpt that can " + "be used to compute setbacks or other geospatial siting requirements " + "for a wind turbine/tower (empty list if none exist in the text). The " + "last key is '{key}', which is a boolean that is set to True if the " + "text excerpt provides enough quantitative info to compute setbacks " + "or other geospatial siting requirements for a wind turbine/tower " + "and False otherwise. Geospatial siting is impacted by any of the " + f"following:\n{RESTRICTIONS}" + ) + + IS_UTILITY_SCALE_PROMPT = ( + "You are a legal scholar that reads ordinance text and determines " + "wether it applies to large wind energy systems. Large wind energy " + "systems (WES) may also be referred to as wind turbines, wind energy " + "conversion systems (WECS), wind energy facilities (WEF), wind energy " + "turbines (WET), large wind energy turbines (LWET), utility-scale " + "wind energy turbines (UWET), commercial wind energy systems, or " + "similar. Your client is a commercial wind developer that does not " + "care about ordinances related to private, micro, small, or medium " + "sized wind energy systems. Ignore any text related to such systems. " + "Return your answer in JSON format (not markdown). Your JSON file " + "must include exactly two keys. The first key is 'summary' which " + "contains a string that summarizes the types of wind energy systems " + "the text applies to (if any). The second key is '{key}', which is a " + "boolean that is set to True if any part of the text excerpt is " + "applicable to the large wind energy conversion systems that the " + "client is interested in and False otherwise." + ) + + def __init__(self, structured_llm_caller, text_chunks, num_to_recall=2): + """ + + Parameters + ---------- + structured_llm_caller : elm.ords.llm.StructuredLLMCaller + StructuredLLMCaller instance. Used for structured validation + queries. + text_chunks : list of str + List of strings, each of which represent a chunk of text. + The order of the strings should be the order of the text + chunks. This validator may refer to previous text chunks to + answer validation questions. + num_to_recall : int, optional + Number of chunks to check for each validation call. This + includes the original chunk! For example, if + `num_to_recall=2`, the validator will first check the chunk + at the requested index, and then the previous chunk as well. + By default, ``2``. + """ + super().__init__( + structured_llm_caller=structured_llm_caller, + text_chunks=text_chunks, + num_to_recall=num_to_recall, + ) + self._legal_text_mem = [] + self._wind_mention_mem = [] + self._ordinance_chunks = [] + + @property + def is_legal_text(self): + """bool: ``True`` if text was found to be from a legal source.""" + if not self._legal_text_mem: + return False + return sum(self._legal_text_mem) >= 0.5 * len(self._legal_text_mem) + + @property + def ordinance_text(self): + """str: Combined ordinance text from the individual chunks.""" + inds_to_grab = set() + for info in self._ordinance_chunks: + inds_to_grab |= { + info["ind"] + x for x in range(1 - self.num_to_recall, 2) + } + + text = [ + self.text_chunks[ind] + for ind in sorted(inds_to_grab) + if 0 <= ind < len(self.text_chunks) + ] + return merge_overlapping_texts(text) + + async def parse(self, min_chunks_to_process=3): + """Parse text chunks and look for ordinance text. + + Parameters + ---------- + min_chunks_to_process : int, optional + Minimum number of chunks to process before checking if + document resembles legal text and ignoring chunks that don't + pass the wind heuristic. By default, ``3``. + + Returns + ------- + bool + ``True`` if any ordinance text was found in the chunks. + """ + for ind, text in enumerate(self.text_chunks): + self._wind_mention_mem.append(possibly_mentions_wind(text)) + if ind >= min_chunks_to_process: + if not self.is_legal_text: + return False + + # fmt: off + if not any(self._wind_mention_mem[-self.num_to_recall:]): + continue + + logger.debug("Processing text at ind %d", ind) + logger.debug("Text:\n%s", text) + + if ind < min_chunks_to_process: + is_legal_text = await self.parse_from_ind( + ind, self.IS_LEGAL_TEXT_PROMPT, key="legal_text" + ) + self._legal_text_mem.append(is_legal_text) + if not is_legal_text: + logger.debug("Text at ind %d is not legal text", ind) + continue + + contains_ord_info = await self.parse_from_ind( + ind, self.CONTAINS_ORD_PROMPT, key="contains_ord_info" + ) + if not contains_ord_info: + logger.debug( + "Text at ind %d does not contain ordinance info", ind + ) + continue + + is_utility_scale = await self.parse_from_ind( + ind, self.IS_UTILITY_SCALE_PROMPT, key="x" + ) + if not is_utility_scale: + logger.debug( + "Text at ind %d is not for utility-scale WECS", ind + ) + continue + + self._ordinance_chunks.append({"text": text, "ind": ind}) + logger.debug("Added text at ind %d to ordinances", ind) + # mask, since we got a good result + self._wind_mention_mem[-1] = False + + return bool(self._ordinance_chunks) + + +class OrdinanceExtractor: + """Extract succinct ordinance text from input""" + + SYSTEM_MESSAGE = ( + "You extract one or more direct excerpts from a given text based on " + "the user's request. Maintain all original formatting and characters " + "without any paraphrasing. If the relevant text is inside of a " + "space-delimited table, return the entire table with the original " + "space-delimited formatting. Never paraphrase! Only return portions " + "of the original text directly." + ) + MODEL_INSTRUCTIONS_RESTRICTIONS = ( + "Extract one or more direct text excerpts related to the restrictions " + "of large wind energy systems with respect to any of the following:\n" + f"{RESTRICTIONS}" + "Include section headers (if any) for the text excerpts. Also include " + "any text excerpts that define what kind of large wind energy " + "conversion system the restriction applies to. If there is no text " + "related to siting restrictions of large wind systems, simply say: " + '"No relevant text."' + ) + MODEL_INSTRUCTIONS_SIZE = ( + "Extract one or more direct text excerpts pertaining to large wind " + "energy systems. Large wind energy systems (WES) may also be referred " + "to as wind turbines, wind energy conversion systems (WECS), wind " + "energy facilities (WEF), wind energy turbines (WET), large wind " + "energy turbines (LWET), utility-scale wind energy turbines (UWET), " + "or similar. Do not return any text excerpts that only apply to " + "private, micro, small, or medium sized wind energy systems. Include " + "section headers (if any) for the text excerpts. Also include any " + "text excerpts that define what kind of large wind energy conversion " + "system the restriction applies to. If there is no text pertaining to " + "large wind systems, simply say: " + '"No relevant text."' + ) + + def __init__(self, llm_caller): + """ + + Parameters + ---------- + llm_caller : elm.ords.llm.LLMCaller + LLM Caller instance used to extract ordinance info with. + """ + self.llm_caller = llm_caller + + async def _process(self, text_chunks, instructions, valid_chunk): + """Perform extraction processing.""" + logger.info( + "Extracting ordinance text from %d text chunks asynchronously...", + len(text_chunks), + ) + outer_task_name = asyncio.current_task().get_name() + summaries = [ + asyncio.create_task( + self.llm_caller.call( + sys_msg=self.SYSTEM_MESSAGE, + content=f"Text:\n{chunk}\n{instructions}", + usage_sub_label="document_ordinance_summary", + ), + name=outer_task_name, + ) + for chunk in text_chunks + ] + summary_chunks = await asyncio.gather(*summaries) + summary_chunks = [ + chunk for chunk in summary_chunks if valid_chunk(chunk) + ] + + text_summary = "\n".join(summary_chunks) + logger.debug( + "Final summary contains %d tokens", + ApiBase.count_tokens( + text_summary, + model=self.llm_caller.kwargs.get("model", "gpt-4"), + ), + ) + return text_summary + + async def check_for_restrictions(self, text_chunks): + """Extract restriction ordinance text from input text chunks. + + Parameters + ---------- + text_chunks : list of str + List of strings, each of which represent a chunk of text. + The order of the strings should be the order of the text + chunks. + + Returns + ------- + str + Ordinance text extracted from text chunks. + """ + return await self._process( + text_chunks=text_chunks, + instructions=self.MODEL_INSTRUCTIONS_RESTRICTIONS, + valid_chunk=_valid_chunk_not_short, + ) + + async def check_for_correct_size(self, text_chunks): + """Extract ordinance text from input text chunks for large WES. + + Parameters + ---------- + text_chunks : list of str + List of strings, each of which represent a chunk of text. + The order of the strings should be the order of the text + chunks. + + Returns + ------- + str + Ordinance text extracted from text chunks. + """ + return await self._process( + text_chunks=text_chunks, + instructions=self.MODEL_INSTRUCTIONS_SIZE, + valid_chunk=_valid_chunk, + ) + + +def _valid_chunk(chunk): + """True if chunk has content.""" + return chunk and "no relevant text" not in chunk.lower() + + +def _valid_chunk_not_short(chunk): + """True if chunk has content and is not too short.""" + return _valid_chunk(chunk) and len(chunk) > 20 diff --git a/elm/ords/extraction/parse.py b/elm/ords/extraction/parse.py new file mode 100644 index 00000000..d91af2f2 --- /dev/null +++ b/elm/ords/extraction/parse.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance structured parsing class.""" +import asyncio +import logging +from copy import deepcopy +from itertools import chain + +import pandas as pd + +from elm.ords.llm.calling import BaseLLMCaller, ChatLLMCaller +from elm.ords.utilities import llm_response_as_json +from elm.ords.extraction.tree import AsyncDecisionTree +from elm.ords.extraction.features import SetbackFeatures +from elm.ords.extraction.graphs import ( + EXTRACT_ORIGINAL_TEXT_PROMPT, + setup_graph_wes_types, + setup_base_graph, + setup_multiplier, + setup_conditional, + setup_participating_owner, + setup_graph_extra_restriction, + llm_response_starts_with_yes, +) + + +logger = logging.getLogger(__name__) + +DEFAULT_SYSTEM_MESSAGE = ( + "You are a legal scholar explaining legal ordinances to a wind " + "energy developer." +) +SETBACKS_SYSTEM_MESSAGE = ( + f"{DEFAULT_SYSTEM_MESSAGE} " + "For the duration of this conversation, only focus on " + "ordinances relating to setbacks from {feature} for {wes_type}. Ignore " + "all text that pertains to private, micro, small, or medium sized wind " + "energy systems." +) +RESTRICTIONS_SYSTEM_MESSAGE = ( + f"{DEFAULT_SYSTEM_MESSAGE} " + "For the duration of this conversation, only focus on " + "ordinances relating to {restriction} for {wes_type}. Ignore " + "all text that pertains to private, micro, small, or medium sized wind " + "energy systems." +) +EXTRA_RESTRICTIONS_TO_CHECK = { + "noise": "maximum noise level", + "max height": "maximum turbine height", + "min lot size": "minimum lot size", + "shadow flicker": "maximum shadow flicker", + "density": "maximum turbine spacing", +} + + +def _setup_async_decision_tree(graph_setup_func, **kwargs): + """Setup Async Decision tree dor ordinance extraction.""" + G = graph_setup_func(**kwargs) + tree = AsyncDecisionTree(G) + assert len(tree.chat_llm_caller.messages) == 1 + return tree + + +def _found_ord(messages): + """Check if ordinance was found based on messages from the base graph. + IMPORTANT: This function may break if the base graph structure changes. + Always update the hardcoded values to match the base graph message + containing the LLM response about ordinance content. + """ + if len(messages) < 3: + return False + return llm_response_starts_with_yes(messages[2].get("content", "")) + + +async def _run_async_tree(tree, response_as_json=True): + """Run Async Decision Tree and return output as dict.""" + try: + response = await tree.async_run() + except RuntimeError: + logger.error( + " - NOTE: This is not necessarily an error and may just mean " + "that the text does not have the requested data." + ) + response = None + + if response_as_json: + return llm_response_as_json(response) if response else {} + + return response + + +async def _run_async_tree_with_bm(tree, base_messages): + """Run Async Decision Tree from base messages and return dict output.""" + tree.chat_llm_caller.messages = base_messages + assert len(tree.chat_llm_caller.messages) == len(base_messages) + return await _run_async_tree(tree) + + +def _empty_output(feature): + """Empty output for a feature (not found in text).""" + if feature in {"struct", "pline"}: + return [ + {"feature": f"{feature} (participating)"}, + {"feature": f"{feature} (non-participating)"}, + ] + return [{"feature": feature}] + + +class StructuredOrdinanceParser(BaseLLMCaller): + """LLM ordinance document structured data scraping utility.""" + + def _init_chat_llm_caller(self, system_message): + """Initialize a ChatLLMCaller instance for the DecisionTree""" + return ChatLLMCaller( + self.llm_service, + system_message=system_message, + usage_tracker=self.usage_tracker, + **self.kwargs, + ) + + async def parse(self, text): + """Parse text and extract structure ordinance data. + + Parameters + ---------- + text : str + Ordinance text which may or may not contain setbacks for one + or more features (property lines, structure, roads, etc.). + Text can also contain other supported regulations (noise, + shadow-flicker, etc,) which will be extracted as well. + + Returns + ------- + pd.DataFrame + DataFrame containing parsed-out ordinance values. + """ + largest_wes_type = await self._check_wind_turbine_type(text) + logger.info("Largest WES type found in text: %s", largest_wes_type) + + outer_task_name = asyncio.current_task().get_name() + feature_parsers = [ + asyncio.create_task( + self._parse_setback_feature( + text, feature_kwargs, largest_wes_type + ), + name=outer_task_name, + ) + for feature_kwargs in SetbackFeatures() + ] + extras_parsers = [ + asyncio.create_task( + self._parse_extra_restriction( + text, feature, r_text, largest_wes_type + ), + name=outer_task_name, + ) + for feature, r_text in EXTRA_RESTRICTIONS_TO_CHECK.items() + ] + outputs = await asyncio.gather(*(feature_parsers + extras_parsers)) + + return pd.DataFrame(chain.from_iterable(outputs)) + + async def _check_wind_turbine_type(self, text): + """Get the largest turbine size mentioned in the text.""" + logger.debug("Checking turbine_types") + tree = _setup_async_decision_tree( + setup_graph_wes_types, + text=text, + chat_llm_caller=self._init_chat_llm_caller(DEFAULT_SYSTEM_MESSAGE), + ) + dtree_wes_types_out = await _run_async_tree(tree) + + largest_wes_type = ( + dtree_wes_types_out.get("largest_wes_type") + or "large wind energy systems" + ) + return largest_wes_type + + async def _parse_extra_restriction( + self, text, feature, restriction_text, largest_wes_type + ): + """Parse a non-setback restriction from the text.""" + logger.debug("Parsing extra feature %r", feature) + system_message = RESTRICTIONS_SYSTEM_MESSAGE.format( + restriction=restriction_text, wes_type=largest_wes_type + ) + tree = _setup_async_decision_tree( + setup_graph_extra_restriction, + wes_type=largest_wes_type, + restriction=restriction_text, + text=text, + chat_llm_caller=self._init_chat_llm_caller(system_message), + ) + info = await _run_async_tree(tree) + info.update({"feature": feature}) + return [info] + + async def _parse_setback_feature( + self, text, feature_kwargs, largest_wes_type + ): + """Parse values for a setback feature.""" + feature = feature_kwargs["feature_id"] + feature_kwargs["wes_type"] = largest_wes_type + logger.debug("Parsing feature %r", feature) + + base_messages = await self._base_messages(text, **feature_kwargs) + if not _found_ord(base_messages): + logger.debug("Failed `_found_ord` check for feature %r", feature) + return _empty_output(feature) + + if feature not in {"struct", "pline"}: + output = {"feature": feature} + output.update( + await self._extract_setback_values( + text, + base_messages=base_messages, + **feature_kwargs, + ) + ) + return [output] + + return await self._extract_setback_values_for_p_or_np( + text, base_messages, **feature_kwargs + ) + + async def _base_messages(self, text, **feature_kwargs): + """Get base messages for setback feature parsing.""" + system_message = SETBACKS_SYSTEM_MESSAGE.format( + feature=feature_kwargs["feature"], + wes_type=feature_kwargs["wes_type"], + ) + tree = _setup_async_decision_tree( + setup_base_graph, + text=text, + chat_llm_caller=self._init_chat_llm_caller(system_message), + **feature_kwargs, + ) + await _run_async_tree(tree, response_as_json=False) + return deepcopy(tree.chat_llm_caller.messages) + + async def _extract_setback_values_for_p_or_np( + self, text, base_messages, **feature_kwargs + ): + """Extract setback values for participating/non-participating ords.""" + logger.debug("Checking participating vs non-participating") + dtree_participating_out = await self._run_setback_graph( + setup_participating_owner, + text, + base_messages=deepcopy(base_messages), + **feature_kwargs, + ) + outer_task_name = asyncio.current_task().get_name() + p_or_np_parsers = [ + asyncio.create_task( + self._parse_p_or_np_text( + key, sub_text, base_messages, **feature_kwargs + ), + name=outer_task_name, + ) + for key, sub_text in dtree_participating_out.items() + ] + return await asyncio.gather(*p_or_np_parsers) + + async def _parse_p_or_np_text( + self, key, sub_text, base_messages, **feature_kwargs + ): + """Parse participating/non-participating sub-text for ord values.""" + feature = feature_kwargs["feature_id"] + out_feat_name = f"{feature} ({key})" + output = {"feature": out_feat_name} + if not sub_text: + return output + + feature = feature_kwargs["feature"] + feature = f"{key} {feature}" + feature_kwargs["feature"] = feature + + base_messages = deepcopy(base_messages) + base_messages[-2]["content"] = EXTRACT_ORIGINAL_TEXT_PROMPT.format( + feature=feature, wes_type=feature_kwargs["wes_type"] + ) + base_messages[-1]["content"] = sub_text + + values = await self._extract_setback_values( + sub_text, + base_messages=base_messages, + **feature_kwargs, + ) + output.update(values) + return output + + async def _extract_setback_values(self, text, **kwargs): + """Extract setback values for a particular feature from input text.""" + dtree_out = await self._run_setback_graph( + setup_multiplier, text, **kwargs + ) + + if dtree_out.get("mult_value") is None: + return dtree_out + + dtree_con_out = await self._run_setback_graph( + setup_conditional, text, **kwargs + ) + dtree_out.update(dtree_con_out) + return dtree_out + + async def _run_setback_graph( + self, + graphs_setup_func, + text, + feature, + wes_type, + base_messages=None, + **kwargs, + ): + """Generic function to run async tree for ordinance extraction.""" + system_message = SETBACKS_SYSTEM_MESSAGE.format( + feature=feature, wes_type=wes_type + ) + tree = _setup_async_decision_tree( + graphs_setup_func, + feature=feature, + text=text, + chat_llm_caller=self._init_chat_llm_caller(system_message), + **kwargs, + ) + if base_messages: + return await _run_async_tree_with_bm(tree, base_messages) + return await _run_async_tree(tree) diff --git a/elm/ords/extraction/tree.py b/elm/ords/extraction/tree.py new file mode 100644 index 00000000..bb7bff8f --- /dev/null +++ b/elm/ords/extraction/tree.py @@ -0,0 +1,134 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance async decision tree.""" +import networkx as nx +import logging + +from elm.tree import DecisionTree + + +logger = logging.getLogger(__name__) + + +class AsyncDecisionTree(DecisionTree): + """Async class to traverse a directed graph of LLM prompts. Nodes are + prompts and edges are transitions between prompts based on conditions + being met in the LLM response.""" + + def __init__(self, graph): + """Async class to traverse a directed graph of LLM prompts. Nodes are + prompts and edges are transitions between prompts based on conditions + being met in the LLM response. + + Parameters + ---------- + graph : nx.DiGraph + Directed acyclic graph where nodes are LLM prompts and edges are + logical transitions based on the response. Must have high-level + graph attribute "chat_llm_caller" which is a ChatLLMCaller + instance. Nodes should have attribute "prompt" which can have + {format} named arguments that will be filled from the high-level + graph attributes. Edges can have attribute "condition" that is a + callable to be executed on the LLM response text. An edge from a + node without a condition acts as an "else" statement if no other + edge conditions are satisfied. A single edge from node to node + does not need a condition. + """ + self._g = graph + self._history = [] + assert isinstance(self.graph, nx.DiGraph) + assert "chat_llm_caller" in self.graph.graph + + @property + def chat_llm_caller(self): + """elm.ords.llm.ChatLLMCaller: ChatLLMCaller instance for this tree.""" + return self.graph.graph["chat_llm_caller"] + + @property + def messages(self): + """Get a list of the conversation messages with the LLM. + + Returns + ------- + list + """ + return self.chat_llm_caller.messages + + @property + def all_messages_txt(self): + """Get a printout of the full conversation with the LLM + + Returns + ------- + str + """ + messages = [ + f"{msg['role'].upper()}: {msg['content']}" for msg in self.messages + ] + messages = "\n\n".join(messages) + return messages + + async def async_call_node(self, node0): + """Call the LLM with the prompt from the input node and search the + successor edges for a valid transition condition + + Parameters + ---------- + node0 : str + Name of node being executed. + + Returns + ------- + out : str + Next node or LLM response if at a leaf node. + """ + prompt = self._prepare_graph_call(node0) + out = await self.chat_llm_caller.call(prompt, usage_sub_label="dtree") + logger.debug( + "Chat GPT prompt:\n%s\nChat GPT response:\n%s", prompt, out + ) + return self._parse_graph_output(node0, out) + + async def async_run(self, node0="init"): + """Traverse the decision tree starting at the input node. + + Parameters + ---------- + node0 : str + Name of starting node in the graph. This is typically called "init" + + Returns + ------- + out : str + Final response from LLM at the leaf node. + """ + + self._history = [] + + while True: + try: + out = await self.async_call_node(node0) + except Exception as e: + logger.debug( + "Error traversing trees, here's the full " + "conversation printout:\n%s", + self.all_messages_txt, + ) + last_message = self.messages[-1]["content"] + msg = ( + "Ran into an exception when traversing tree. " + "Last message from LLM is printed below. " + "See debug logs for more detail. " + "\nLast message: \n" + '"""\n%s\n"""' + ) + logger.error(msg, last_message) + logger.exception(e) + raise RuntimeError(msg % last_message) from e + if out in self.graph: + node0 = out + else: + break + + logger.info("Output: %s", out) + + return out diff --git a/elm/ords/llm/__init__.py b/elm/ords/llm/__init__.py new file mode 100644 index 00000000..8f7d2a50 --- /dev/null +++ b/elm/ords/llm/__init__.py @@ -0,0 +1,3 @@ +"""ELM Ordinance LLM callers. """ + +from .calling import LLMCaller, ChatLLMCaller, StructuredLLMCaller diff --git a/elm/ords/llm/calling.py b/elm/ords/llm/calling.py new file mode 100644 index 00000000..59f64af5 --- /dev/null +++ b/elm/ords/llm/calling.py @@ -0,0 +1,182 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinances LLM Calling classes.""" +import logging + +from elm.ords.utilities import llm_response_as_json + + +logger = logging.getLogger(__name__) +_JSON_INSTRUCTIONS = "Return your answer in JSON format" + + +class BaseLLMCaller: + """Class to support LLM calling functionality.""" + + def __init__(self, llm_service, usage_tracker=None, **kwargs): + """ + + Parameters + ---------- + llm_service : elm.ords.services.base.Service + LLM service used for queries. + usage_tracker : elm.ords.services.usage.UsageTracker, optional + Optional tracker instance to monitor token usage during + LLM calls. By default, ``None``. + **kwargs + Keyword arguments to be passed to the underlying service + processing function (i.e. `llm_service.call(**kwargs)`). + Should *not* contain the following keys: + + - usage_tracker + - usage_sub_label + - messages + + These arguments are provided by this caller object. + """ + self.llm_service = llm_service + self.usage_tracker = usage_tracker + self.kwargs = kwargs + + +class LLMCaller(BaseLLMCaller): + """Simple LLM caller, with no memory and no parsing utilities.""" + + async def call(self, sys_msg, content, usage_sub_label="default"): + """Call LLM. + + Parameters + ---------- + sys_msg : str + The LLM system message. + content : str + Your chat message for the LLM. + usage_sub_label : str, optional + Label to store token usage under. By default, ``"default"``. + + Returns + ------- + str | None + The LLM response, as a string, or ``None`` if something went + wrong during the call. + """ + response = await self.llm_service.call( + usage_tracker=self.usage_tracker, + usage_sub_label=usage_sub_label, + messages=[ + {"role": "system", "content": sys_msg}, + {"role": "user", "content": content}, + ], + **self.kwargs, + ) + return response + + +class ChatLLMCaller(BaseLLMCaller): + """Class to support chat-like LLM calling functionality.""" + + def __init__( + self, llm_service, system_message, usage_tracker=None, **kwargs + ): + """ + + Parameters + ---------- + llm_service : elm.ords.services.base.Service + LLM service used for queries. + system_message : str + System message to use for chat with LLM. + usage_tracker : elm.ords.services.usage.UsageTracker, optional + Optional tracker instance to monitor token usage during + LLM calls. By default, ``None``. + **kwargs + Keyword arguments to be passed to the underlying service + processing function (i.e. `llm_service.call(**kwargs)`). + Should *not* contain the following keys: + + - usage_tracker + - usage_sub_label + - messages + + These arguments are provided by this caller object. + """ + super().__init__(llm_service, usage_tracker, **kwargs) + self.messages = [{"role": "system", "content": system_message}] + + async def call(self, content, usage_sub_label="chat"): + """Chat with the LLM. + + Parameters + ---------- + content : str + Your chat message for the LLM. + usage_sub_label : str, optional + Label to store token usage under. By default, ``"chat"``. + + Returns + ------- + str | None + The LLM response, as a string, or ``None`` if something went + wrong during the call. + """ + self.messages.append({"role": "user", "content": content}) + + response = await self.llm_service.call( + usage_tracker=self.usage_tracker, + usage_sub_label=usage_sub_label, + messages=self.messages, + **self.kwargs, + ) + if response is None: + self.messages = self.messages[:-1] + return None + + self.messages.append({"role": "assistant", "content": response}) + return response + + +class StructuredLLMCaller(BaseLLMCaller): + """Class to support structured (JSON) LLM calling functionality.""" + + async def call(self, sys_msg, content, usage_sub_label="default"): + """Call LLM for structured data retrieval. + + Parameters + ---------- + sys_msg : str + The LLM system message. If this text does not contain the + instruction text "Return your answer in JSON format", it + will be added. + content : str + LLM call content (typically some text to extract info from). + usage_sub_label : str, optional + Label to store token usage under. By default, ``"default"``. + + Returns + ------- + dict + Dictionary containing the LLM-extracted features. Dictionary + may be empty if there was an error during the LLM call. + """ + sys_msg = _add_json_instructions_if_needed(sys_msg) + + response = await self.llm_service.call( + usage_tracker=self.usage_tracker, + usage_sub_label=usage_sub_label, + messages=[ + {"role": "system", "content": sys_msg}, + {"role": "user", "content": content}, + ], + **self.kwargs, + ) + return llm_response_as_json(response) if response else {} + + +def _add_json_instructions_if_needed(system_message): + """Add JSON instruction to system message if needed.""" + if _JSON_INSTRUCTIONS.casefold() not in system_message.casefold(): + logger.debug( + "JSON instructions not found in system message. Adding..." + ) + system_message = f"{system_message} {_JSON_INSTRUCTIONS}." + logger.debug("New system message:\n%s", system_message) + return system_message diff --git a/elm/ords/process.py b/elm/ords/process.py new file mode 100644 index 00000000..acdc3689 --- /dev/null +++ b/elm/ords/process.py @@ -0,0 +1,715 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance full processing logic""" +import os +import time +import json +import asyncio +import logging +from datetime import datetime, timedelta +from pathlib import Path +from functools import partial + +import openai +import pandas as pd +from langchain.text_splitter import RecursiveCharacterTextSplitter + +from elm import ApiBase +from elm.ords.download import download_county_ordinance +from elm.ords.extraction import ( + extract_ordinance_text_with_ngram_validation, + extract_ordinance_values, +) +from elm.ords.services.usage import UsageTracker +from elm.ords.services.openai import OpenAIService, usage_from_response +from elm.ords.services.provider import RunningAsyncServices +from elm.ords.services.threaded import ( + TempFileCache, + FileMover, + CleanedFileWriter, + OrdDBFileWriter, + UsageUpdater, +) +from elm.ords.services.cpu import PDFLoader, read_pdf_doc, read_pdf_doc_ocr +from elm.ords.utilities import ( + RTS_SEPARATORS, + load_all_county_info, + load_counties_from_fp, +) +from elm.ords.utilities.location import County +from elm.ords.utilities.queued_logging import ( + LocationFileLog, + LogListener, + NoLocationFilter, +) + +logger = logging.getLogger(__name__) + + +OUT_COLS = [ + "county", + "state", + "FIPS", + "feature", + "fixed_value", + "mult_value", + "mult_type", + "adder", + "min_dist", + "max_dist", + "value", + "units", + "ord_year", + "last_updated", + "section", + "source", + "comment", +] + +CHECK_COLS = [ + "fixed_value", + "mult_value", + "adder", + "min_dist", + "max_dist", + "value", +] + + +async def process_counties_with_openai( + out_dir, + county_fp=None, + model="gpt-4", + azure_api_key=None, + azure_version=None, + azure_endpoint=None, + llm_call_kwargs=None, + llm_service_rate_limit=4000, + text_splitter_chunk_size=3000, + text_splitter_chunk_overlap=300, + num_urls_to_check_per_county=5, + max_num_concurrent_browsers=10, + file_loader_kwargs=None, + pytesseract_exe_fp=None, + td_kwargs=None, + tpe_kwargs=None, + ppe_kwargs=None, + log_dir=None, + clean_dir=None, + county_ords_dir=None, + county_dbs_dir=None, + log_level="INFO", +): + """Download and extract ordinances for a list of counties. + + Parameters + ---------- + out_dir : path-like + Path to output directory. This directory will be created if it + does not exist. This directory will contain the structured + ordinance output CSV as well as all of the scraped ordinance + documents (PDFs and HTML text files). Usage information and + default options for log/clean directories will also be stored + here. + county_fp : path-like, optional + Path to CSV file containing a list of counties to extract + ordinance information for. This CSV should have "County" and + "State" columns that contains the county and state names. + By default, ``None``, which runs the extraction for all known + counties (this is untested and not currently recommended). + model : str, optional + Name of LLM model to perform scraping. By default, ``"gpt-4"``. + azure_api_key : str, optional + Azure OpenAI API key. By default, ``None``, which pulls the key + from the environment variable ``AZURE_OPENAI_API_KEY`` instead. + azure_version : str, optional + Azure OpenAI API version. By default, ``None``, which pulls the + version from the environment variable ``AZURE_OPENAI_VERSION`` + instead. + azure_endpoint : str, optional + Azure OpenAI API endpoint. By default, ``None``, which pulls the + endpoint from the environment variable ``AZURE_OPENAI_ENDPOINT`` + instead. + llm_call_kwargs : dict, optional + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. By default, ``None``. + llm_service_rate_limit : int, optional + Token rate limit of LLm service being used (OpenAI). + By default, ``4000``. + text_splitter_chunk_size : int, optional + Chunk size input to + `langchain.text_splitter.RecursiveCharacterTextSplitter`. + By default, ``3000``. + text_splitter_chunk_overlap : int, optional + Chunk overlap input to + `langchain.text_splitter.RecursiveCharacterTextSplitter`. + By default, ``300``. + num_urls_to_check_per_county : int, optional + Number of unique Google search result URL's to check for + ordinance document. By default, ``5``. + max_num_concurrent_browsers : int, optional + Number of unique concurrent browser instances to open when + performing Google search. Setting this number too high on a + machine with limited processing can lead to increased timeouts + and therefore decreased quality of Google search results. + By default, ``10``. + pytesseract_exe_fp : path-like, optional + Path to pytesseract executable. If this option is specified, OCR + parsing for PDf files will be enabled via pytesseract. + By default, ``None``. + td_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`tempfile.TemporaryDirectory`. The temporary directory is + used to store files downloaded from the web that are still being + parsed for ordinance information. By default, ``None``. + tpe_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ThreadPoolExecutor`. The thread pool + executor is used to run I/O intensive tasks like writing to a + log file. By default, ``None``. + ppe_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ProcessPoolExecutor`. The process + pool executor is used to run CPU intensive tasks like loading + a PDF file. By default, ``None``. + log_dir : path-like, optional + Path to directory for log files. This directory will be created + if it does not exist. By default, ``None``, which + creates a ``logs`` folder in the output directory for the + county-specific log files. + clean_dir : path-like, optional + Path to directory for cleaned ordinance text output. This + directory will be created if it does not exist. By default, + ``None``, which creates a ``clean`` folder in the output + directory for the cleaned ordinance text files. + county_ords_dir : path-like, optional + Path to directory for individual county ordinance file outputs. + This directory will be created if it does not exist. + By default, ``None``, which creates a ``county_ord_files`` + folder in the output directory. + county_dbs_dir : path-like, optional + Path to directory for individual county ordinance database + outputs. This directory will be created if it does not exist. + By default, ``None``, which creates a ``county_dbs`` folder in + the output directory. + log_level : str, optional + Log level to set for county retrieval and parsing loggers. + By default, ``"INFO"``. + + Returns + ------- + pd.DataFrame + DataFrame of parsed ordinance information. This file will also + be stored in the output directory under "wind_db.csv". + """ + start_time = time.time() + log_listener = LogListener(["elm"], level=log_level) + dirs = _setup_folders( + out_dir, + log_dir=log_dir, + clean_dir=clean_dir, + cod=county_ords_dir, + cdd=county_dbs_dir, + ) + out_dir, log_dir, clean_dir, county_ords_dir, county_dbs_dir = dirs + async with log_listener as ll: + _setup_main_logging(log_dir, log_level, ll) + db = await _process_with_logs( + out_dir, + log_dir, + clean_dir, + county_ords_dir, + county_dbs_dir, + ll, + county_fp=county_fp, + model=model, + azure_api_key=azure_api_key, + azure_version=azure_version, + azure_endpoint=azure_endpoint, + llm_call_kwargs=llm_call_kwargs, + llm_service_rate_limit=llm_service_rate_limit, + text_splitter_chunk_size=text_splitter_chunk_size, + text_splitter_chunk_overlap=text_splitter_chunk_overlap, + num_urls_to_check_per_county=num_urls_to_check_per_county, + max_num_concurrent_browsers=max_num_concurrent_browsers, + file_loader_kwargs=file_loader_kwargs, + pytesseract_exe_fp=pytesseract_exe_fp, + td_kwargs=td_kwargs, + tpe_kwargs=tpe_kwargs, + ppe_kwargs=ppe_kwargs, + log_level=log_level, + ) + _record_total_time(out_dir / "usage.json", time.time() - start_time) + return db + + +async def _process_with_logs( + out_dir, + log_dir, + clean_dir, + county_ords_dir, + county_dbs_dir, + log_listener, + county_fp=None, + model="gpt-4", + azure_api_key=None, + azure_version=None, + azure_endpoint=None, + llm_call_kwargs=None, + llm_service_rate_limit=4000, + text_splitter_chunk_size=3000, + text_splitter_chunk_overlap=300, + num_urls_to_check_per_county=5, + max_num_concurrent_browsers=10, + file_loader_kwargs=None, + pytesseract_exe_fp=None, + td_kwargs=None, + tpe_kwargs=None, + ppe_kwargs=None, + log_level="INFO", +): + """Process counties with logging enabled.""" + counties = _load_counties_to_process(county_fp) + azure_api_key, azure_version, azure_endpoint = _validate_api_params( + azure_api_key, azure_version, azure_endpoint + ) + + tpe_kwargs = _configure_thread_pool_kwargs(tpe_kwargs) + file_loader_kwargs = _configure_file_loader_kwargs(file_loader_kwargs) + if pytesseract_exe_fp is not None: + _setup_pytesseract(pytesseract_exe_fp) + file_loader_kwargs.update({"pdf_ocr_read_coroutine": read_pdf_doc_ocr}) + + text_splitter = RecursiveCharacterTextSplitter( + RTS_SEPARATORS, + chunk_size=text_splitter_chunk_size, + chunk_overlap=text_splitter_chunk_overlap, + length_function=partial(ApiBase.count_tokens, model=model), + ) + client = openai.AsyncAzureOpenAI( + api_key=azure_api_key, + api_version=azure_version, + azure_endpoint=azure_endpoint, + ) + + services = [ + OpenAIService(client, rate_limit=llm_service_rate_limit), + TempFileCache(td_kwargs=td_kwargs, tpe_kwargs=tpe_kwargs), + FileMover(county_ords_dir, tpe_kwargs=tpe_kwargs), + CleanedFileWriter(clean_dir, tpe_kwargs=tpe_kwargs), + OrdDBFileWriter(county_dbs_dir, tpe_kwargs=tpe_kwargs), + UsageUpdater(out_dir / "usage.json", tpe_kwargs=tpe_kwargs), + PDFLoader(**(ppe_kwargs or {})), + ] + + browser_semaphore = ( + asyncio.Semaphore(max_num_concurrent_browsers) + if max_num_concurrent_browsers + else None + ) + + async with RunningAsyncServices(services): + tasks = [] + trackers = [] + for __, row in counties.iterrows(): + county, state, fips = row[["County", "State", "FIPS"]] + location = County(county.strip(), state=state.strip(), fips=fips) + usage_tracker = UsageTracker( + location.full_name, usage_from_response + ) + trackers.append(usage_tracker) + task = asyncio.create_task( + download_docs_for_county_with_logging( + log_listener, + log_dir, + location, + text_splitter, + num_urls=num_urls_to_check_per_county, + file_loader_kwargs=file_loader_kwargs, + browser_semaphore=browser_semaphore, + level=log_level, + llm_service=OpenAIService, + usage_tracker=usage_tracker, + model=model, + **(llm_call_kwargs or {}), + ), + name=location.full_name, + ) + tasks.append(task) + docs = await asyncio.gather(*tasks) + + db = _docs_to_db(docs) + db.to_csv(out_dir / "wind_db.csv", index=False) + return db + + +def _setup_main_logging(log_dir, level, listener): + """Setup main logger for catching exceptions during execution.""" + handler = logging.FileHandler(log_dir / "main.log", encoding="utf-8") + handler.setLevel(level) + handler.addFilter(NoLocationFilter()) + listener.addHandler(handler) + + +def _setup_folders( + out_dir, + log_dir=None, + clean_dir=None, + cod=None, + cdd=None, +): + """Setup output directory folders.""" + out_dir = Path(out_dir) + out_folders = [ + out_dir, + Path(log_dir) if log_dir else out_dir / "logs", + Path(clean_dir) if clean_dir else out_dir / "clean", + Path(cod) if cod else out_dir / "county_ord_files", + Path(cdd) if cdd else out_dir / "county_dbs", + ] + for folder in out_folders: + folder.mkdir(exist_ok=True, parents=True) + return out_folders + + +def _load_counties_to_process(county_fp): + """Load the counties to retrieve documents for.""" + if county_fp is None: + logger.info("No `county_fp` input! Loading all counties") + return load_all_county_info() + return load_counties_from_fp(county_fp) + + +def _validate_api_params(azure_api_key, azure_version, azure_endpoint): + """Validate OpenAI API parameters.""" + azure_api_key = azure_api_key or os.environ.get("AZURE_OPENAI_API_KEY") + azure_version = azure_version or os.environ.get("AZURE_OPENAI_VERSION") + azure_endpoint = azure_endpoint or os.environ.get("AZURE_OPENAI_ENDPOINT") + assert azure_api_key is not None, "Must set AZURE_OPENAI_API_KEY!" + assert azure_version is not None, "Must set AZURE_OPENAI_VERSION!" + assert azure_endpoint is not None, "Must set AZURE_OPENAI_ENDPOINT!" + return azure_api_key, azure_version, azure_endpoint + + +def _configure_thread_pool_kwargs(tpe_kwargs): + """Set thread pool workers to 5 if user didn't specify.""" + tpe_kwargs = tpe_kwargs or {} + tpe_kwargs.setdefault("max_workers", 5) + return tpe_kwargs + + +def _configure_file_loader_kwargs(file_loader_kwargs): + """Add PDF reading coroutine to kwargs.""" + file_loader_kwargs = file_loader_kwargs or {} + file_loader_kwargs.update({"pdf_read_coroutine": read_pdf_doc}) + return file_loader_kwargs + + +async def download_docs_for_county_with_logging( + listener, + log_dir, + county, + text_splitter, + num_urls=5, + file_loader_kwargs=None, + browser_semaphore=None, + level="INFO", + **kwargs, +): + """Retrieve ordinance document for a single county with async logs. + + Parameters + ---------- + listener : elm.ords.utilities.queued_logging.LogListener + Active ``LogListener`` instance that can be passed to + :class:`elm.ords.utilities.queued_logging.LocationFileLog`. + log_dir : path-like + Path to output directory to contain log file. + county : elm.ords.utilities.location.Location + County to retrieve ordinance document for. + text_splitter : obj, optional + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. Langchain's text splitters should work for this + input. + num_urls : int, optional + Number of unique Google search result URL's to check for + ordinance document. By default, ``5``. + file_loader_kwargs : dict, optional + Dictionary of keyword-argument pairs to initialize + :class:`elm.web.file_loader.AsyncFileLoader` with. The + "pw_launch_kwargs" key in these will also be used to initialize + the :class:`elm.web.google_search.PlaywrightGoogleLinkSearch` + used for the google URL search. By default, ``None``. + browser_semaphore : asyncio.Semaphore, optional + Semaphore instance that can be used to limit the number of + playwright browsers open concurrently. If ``None``, no limits + are applied. By default, ``None``. + level : str, optional + Log level to set for retrieval logger. By default, ``"INFO"``. + **kwargs + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. + + Returns + ------- + elm.web.document.BaseDocument | None + Document instance for the ordinance document, or ``None`` if no + document was found. Extracted ordinance information is stored in + the document's ``metadata`` attribute. + """ + with LocationFileLog( + listener, log_dir, location=county.full_name, level=level + ): + task = asyncio.create_task( + download_doc_for_county( + county, + text_splitter, + num_urls=num_urls, + file_loader_kwargs=file_loader_kwargs, + browser_semaphore=browser_semaphore, + **kwargs, + ), + name=county.full_name, + ) + try: + doc, *__ = await asyncio.gather(task) + except KeyboardInterrupt: + raise + except Exception as e: + logger.error( + "Encountered error while processing %s:", county.full_name + ) + logger.exception(e) + doc = None + + return doc + + +async def download_doc_for_county( + county, + text_splitter, + num_urls=5, + file_loader_kwargs=None, + browser_semaphore=None, + **kwargs, +): + """Download and parse ordinance document for a single county. + + Parameters + ---------- + county : elm.ords.utilities.location.Location + County to retrieve ordinance document for. + text_splitter : obj, optional + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. Langchain's text splitters should work for this + input. + num_urls : int, optional + Number of unique Google search result URL's to check for + ordinance document. By default, ``5``. + file_loader_kwargs : dict, optional + Dictionary of keyword-argument pairs to initialize + :class:`elm.web.file_loader.AsyncFileLoader` with. The + "pw_launch_kwargs" key in these will also be used to initialize + the :class:`elm.web.google_search.PlaywrightGoogleLinkSearch` + used for the google URL search. By default, ``None``. + browser_semaphore : asyncio.Semaphore, optional + Semaphore instance that can be used to limit the number of + playwright browsers open concurrently. If ``None``, no limits + are applied. By default, ``None``. + **kwargs + Keyword-value pairs used to initialize an + `elm.ords.llm.LLMCaller` instance. + + Returns + ------- + elm.web.document.BaseDocument | None + Document instance for the ordinance document, or ``None`` if no + document was found. Extracted ordinance information is stored in + the document's ``metadata`` attribute. + """ + start_time = time.time() + doc = await download_county_ordinance( + county, + text_splitter, + num_urls=num_urls, + file_loader_kwargs=file_loader_kwargs, + browser_semaphore=browser_semaphore, + **kwargs, + ) + if doc is None: + await _record_time_and_usage(start_time, **kwargs) + return None + + doc.metadata["location"] = county + doc.metadata["location_name"] = county.full_name + await _record_usage(**kwargs) + + doc = await extract_ordinance_text_with_ngram_validation( + doc, text_splitter, **kwargs + ) + await _record_usage(**kwargs) + + doc = await _write_cleaned_text(doc) + doc = await extract_ordinance_values(doc, **kwargs) + + ord_count = _num_ords_in_doc(doc) + if ord_count > 0: + doc = await _move_file_to_out_dir(doc) + doc = await _write_ord_db(doc) + logger.info( + "%d ordinance value(s) found for %s. Outputs are here: '%s'", + ord_count, + county.full_name, + doc.metadata["ord_db_fp"], + ) + else: + logger.info("No ordinances found for %s.", county.full_name) + + await _record_time_and_usage(start_time, **kwargs) + return doc + + +async def _record_usage(**kwargs): + """Dump usage to file if tracker found in kwargs.""" + usage_tracker = kwargs.get("usage_tracker") + if usage_tracker: + await UsageUpdater.call(usage_tracker) + + +async def _record_time_and_usage(start_time, **kwargs): + """Add elapsed time before updating usage to file.""" + seconds_elapsed = time.time() - start_time + usage_tracker = kwargs.get("usage_tracker") + if usage_tracker: + usage_tracker["total_time_seconds"] = seconds_elapsed + usage_tracker["total_time"] = str(timedelta(seconds=seconds_elapsed)) + await UsageUpdater.call(usage_tracker) + + +async def _move_file_to_out_dir(doc): + """Move PDF or HTML text file to output directory.""" + out_fp = await FileMover.call(doc) + doc.metadata["out_fp"] = out_fp + return doc + + +async def _write_cleaned_text(doc): + """Write cleaned text to `clean_dir`.""" + out_fp = await CleanedFileWriter.call(doc) + doc.metadata["cleaned_fp"] = out_fp + return doc + + +async def _write_ord_db(doc): + """Write cleaned text to `county_dbs_dir`.""" + out_fp = await OrdDBFileWriter.call(doc) + doc.metadata["ord_db_fp"] = out_fp + return doc + + +def _setup_pytesseract(exe_fp): + """Set the pytesseract command.""" + import pytesseract + + logger.debug("Setting `tesseract_cmd` to %s", exe_fp) + pytesseract.pytesseract.tesseract_cmd = exe_fp + + +def _record_total_time(fp, seconds_elapsed): + """Dump usage to an existing file.""" + if not Path(fp).exists(): + usage_info = {} + else: + with open(fp, "r") as fh: + usage_info = json.load(fh) + + total_time_str = str(timedelta(seconds=seconds_elapsed)) + usage_info["total_time_seconds"] = seconds_elapsed + usage_info["total_time"] = total_time_str + + with open(fp, "w") as fh: + json.dump(usage_info, fh, indent=4) + + logger.info("Total processing time: %s", total_time_str) + + +def _num_ords_in_doc(doc): + """Check if doc contains any scraped ordinance values.""" + if doc is None: + return 0 + + if "ordinance_values" not in doc.metadata: + return 0 + + ord_vals = doc.metadata["ordinance_values"] + if ord_vals.empty: + return 0 + + check_cols = [col for col in CHECK_COLS if col in ord_vals] + if not check_cols: + return 0 + + return (~ord_vals[check_cols].isna()).values.sum(axis=1).sum() + + +def _docs_to_db(docs): + """Convert list of docs to output database.""" + db = [] + for doc in docs: + if doc is None or isinstance(doc, Exception): + continue + + if _num_ords_in_doc(doc) == 0: + continue + + results = _db_results(doc) + results = _formatted_db(results) + db.append(results) + + if not db: + return pd.DataFrame(columns=OUT_COLS) + + db = pd.concat(db) + db = _empirical_adjustments(db) + return _formatted_db(db) + + +def _db_results(doc): + """Extract results from doc metadata to DataFrame.""" + results = doc.metadata.get("ordinance_values") + if results is None: + return None + + results["source"] = doc.metadata.get("source") + year = doc.metadata.get("date", (None, None, None))[0] + results["ord_year"] = year if year is not None and year > 0 else None + results["last_updated"] = datetime.now().strftime("%m/%d/%Y") + + location = doc.metadata["location"] + results["FIPS"] = location.fips + results["county"] = location.name + results["state"] = location.state + return results + + +def _empirical_adjustments(db): + """Post-processing adjustments based on empirical observations. + + Current adjustments include: + + - Limit adder to max of 250 ft. + - Chat GPT likes to report large values here, but in + practice all values manually observed in ordinance documents + are below 250 ft. + + """ + if "adder" in db.columns: + db.loc[db["adder"] > 250, "adder"] = None + return db + + +def _formatted_db(db): + """Format DataFrame for output.""" + out_cols = [col for col in OUT_COLS if col in db.columns] + return db[out_cols] diff --git a/elm/ords/services/__init__.py b/elm/ords/services/__init__.py new file mode 100644 index 00000000..325e9ea4 --- /dev/null +++ b/elm/ords/services/__init__.py @@ -0,0 +1 @@ +"""ELM asynchronous services. """ diff --git a/elm/ords/services/base.py b/elm/ords/services/base.py new file mode 100644 index 00000000..c8c9f9b4 --- /dev/null +++ b/elm/ords/services/base.py @@ -0,0 +1,126 @@ +# -*- coding: utf-8 -*- +"""ELM abstract Service class.""" +import asyncio +import logging +from abc import ABC, abstractmethod + +from elm.ords.services.queues import get_service_queue +from elm.ords.utilities.exceptions import ELMOrdsNotInitializedError + + +logger = logging.getLogger(__name__) + + +class Service(ABC): + """Abstract base class for a Service that can be queued to run.""" + + MAX_CONCURRENT_JOBS = 10_000 + """Max number of concurrent job submissions.""" + + @classmethod + def _queue(cls): + """Get queue for class.""" + queue = get_service_queue(cls.__name__) + if queue is None: + raise ELMOrdsNotInitializedError("Must initialize the queue!") + return queue + + @classmethod + async def call(cls, *args, **kwargs): + """Call the service. + + Parameters + ---------- + *args, **kwargs + Positional and keyword arguments to be passed to the + underlying service processing function. + + Returns + ------- + obj + A response object from the underlying service. + """ + fut = asyncio.Future() + outer_task_name = asyncio.current_task().get_name() + await cls._queue().put((fut, outer_task_name, args, kwargs)) + return await fut + + @property + def name(self): + """str: Service name used to pull the correct queue object.""" + return self.__class__.__name__ + + async def process_using_futures(self, fut, *args, **kwargs): + """Process a call to the service. + + Parameters + ---------- + fut : asyncio.Future + A future object that should get the result of the processing + operation. If the processing function returns ``answer``, + this method should call ``fut.set_result(answer)``. + **kwargs + Keyword arguments to be passed to the + underlying processing function. + """ + + try: + response = await self.process(*args, **kwargs) + except Exception as e: + fut.set_exception(e) + return + + fut.set_result(response) + + def acquire_resources(self): + """Use this method to allocate resources, if needed""" + + def release_resources(self): + """Use this method to clean up resources, if needed""" + + @property + @abstractmethod + def can_process(self): + """Check if process function can be called. + + This should be a fast-running method that returns a boolean + indicating wether or not the service can accept more + processing calls. + """ + + @abstractmethod + async def process(self, *args, **kwargs): + """Process a call to the service. + + Parameters + ---------- + *args, **kwargs + Positional and keyword arguments to be passed to the + underlying processing function. + """ + + +class RateLimitedService(Service): + """Abstract Base Class representing a rate-limited service (e.g. OpenAI)""" + + def __init__(self, rate_limit, rate_tracker): + """ + + Parameters + ---------- + rate_limit : int | float + Max usage per duration of the rate tracker. For example, + if the rate tracker is set to compute the total over + minute-long intervals, this value should be the max usage + per minute. + rate_tracker : `elm.ords.utilities.usage.TimeBoundedUsageTracker` + A TimeBoundedUsageTracker instance. This will be used to + track usage per time interval and compare to `rate_limit`. + """ + self.rate_limit = rate_limit + self.rate_tracker = rate_tracker + + @property + def can_process(self): + """Check if usage is under the rate limit.""" + return self.rate_tracker.total < self.rate_limit diff --git a/elm/ords/services/cpu.py b/elm/ords/services/cpu.py new file mode 100644 index 00000000..d25e27e6 --- /dev/null +++ b/elm/ords/services/cpu.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance CPU-bound services""" +import asyncio +from functools import partial +from concurrent.futures import ProcessPoolExecutor + +from elm.ords.services.base import Service +from elm.web.document import PDFDocument +from elm.utilities.parse import read_pdf, read_pdf_ocr + + +class ProcessPoolService(Service): + """Service that contains a ProcessPoolExecutor instance""" + + def __init__(self, **kwargs): + """ + + Parameters + ---------- + **kwargs + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ProcessPoolExecutor`. + By default, ``None``. + """ + self._ppe_kwargs = kwargs or {} + self.pool = None + + def acquire_resources(self): + """Open thread pool and temp directory""" + self.pool = ProcessPoolExecutor(**self._ppe_kwargs) + + def release_resources(self): + """Shutdown thread pool and cleanup temp directory""" + self.pool.shutdown(wait=True, cancel_futures=True) + + +class PDFLoader(ProcessPoolService): + """Class to load PDFs in a ProcessPoolExecutor.""" + + @property + def can_process(self): + """bool: Always ``True`` (limiting is handled by asyncio)""" + return True + + async def process(self, fn, pdf_bytes, **kwargs): + """Write URL doc to file asynchronously. + + Parameters + ---------- + doc : elm.web.document.Document + Document containing meta information about the file. Must + have a "source" key in the `metadata` dict containing the + URL, which will be converted to a file name using + :func:`compute_fn_from_url`. + file_content : str | bytes + File content, typically string text for HTML files and bytes + for PDF file. + make_name_unique : bool, optional + Option to make file name unique by adding a UUID at the end + of the file name. By default, ``False``. + + Returns + ------- + Path + Path to output file. + """ + loop = asyncio.get_running_loop() + result = await loop.run_in_executor( + self.pool, partial(fn, pdf_bytes, **kwargs) + ) + return result + + +def _read_pdf(pdf_bytes, **kwargs): + """Utility function so that pdftotext.PDF doesn't have to be pickled.""" + pages = read_pdf(pdf_bytes, verbose=False) + return PDFDocument(pages, **kwargs) + + +def _read_pdf_ocr(pdf_bytes, tesseract_cmd, **kwargs): + """Utility function that mimics `_read_pdf`.""" + if tesseract_cmd: + _configure_pytesseract(tesseract_cmd) + + pages = read_pdf_ocr(pdf_bytes, verbose=True) + return PDFDocument(pages, **kwargs) + + +def _configure_pytesseract(tesseract_cmd): + """Set the tesseract_cmd""" + import pytesseract + + pytesseract.pytesseract.tesseract_cmd = tesseract_cmd + + +async def read_pdf_doc(pdf_bytes, **kwargs): + """Read PDF file from bytes in a Process Pool. + + Parameters + ---------- + pdf_bytes : bytes + Bytes containing PDF file. + **kwargs + Keyword-value arguments to pass to + :class:`elm.web.document.PDFDocument` initializer. + + Returns + ------- + elm.web.document.PDFDocument + PDFDocument instances with pages loaded as text. + """ + return await PDFLoader.call(_read_pdf, pdf_bytes, **kwargs) + + +async def read_pdf_doc_ocr(pdf_bytes, **kwargs): + """Read PDF file from bytes using OCR (pytesseract) in a Process Pool. + + Note that Pytesseract must be set up properly for this method to + work. In particular, the `pytesseract.pytesseract.tesseract_cmd` + attribute must be set to point to the pytesseract exe. + + Parameters + ---------- + pdf_bytes : bytes + Bytes containing PDF file. + **kwargs + Keyword-value arguments to pass to + :class:`elm.web.document.PDFDocument` initializer. + + Returns + ------- + elm.web.document.PDFDocument + PDFDocument instances with pages loaded as text. + """ + import pytesseract + + return await PDFLoader.call( + _read_pdf_ocr, + pdf_bytes, + tesseract_cmd=pytesseract.pytesseract.tesseract_cmd, + **kwargs + ) diff --git a/elm/ords/services/openai.py b/elm/ords/services/openai.py new file mode 100644 index 00000000..ce0a66d2 --- /dev/null +++ b/elm/ords/services/openai.py @@ -0,0 +1,169 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinances OpenAI service amd utils.""" +import logging + +import openai + +from elm.base import ApiBase +from elm.ords.services.base import RateLimitedService +from elm.ords.services.usage import TimeBoundedUsageTracker +from elm.utilities.retry import async_retry_with_exponential_backoff + + +logger = logging.getLogger(__name__) + + +def usage_from_response(current_usage, response): + """OpenAI usage parser. + + Parameters + ---------- + current_usage : dict + Dictionary containing current usage information. For OpenAI + trackers, this may contain the keys ``"requests"``, + ``"prompt_tokens"``, and ``"response_tokens"`` if there is + already existing tracking information. Empty dictionaries are + allowed, in which case the three keys above will be added to + this input. + response : openai.Completion + OpenAI Completion object. Must contain a ``usage`` attribute + that + + Returns + ------- + dict + Dictionary with updated usage statistics. + """ + current_usage["requests"] = current_usage.get("requests", 0) + 1 + current_usage["prompt_tokens"] = ( + current_usage.get("prompt_tokens", 0) + response.usage.prompt_tokens + ) + current_usage["response_tokens"] = ( + current_usage.get("response_tokens", 0) + + response.usage.completion_tokens + ) + return current_usage + + +def count_tokens(messages, model): + """Count the number of tokens in an outgoing set of messages. + + Parameters + ---------- + messages : list + A list of message objects, where the latter is represented + using a dictionary. Each message dictionary must have a + "content" key containing the string to count tokens for. + model : str + The OpenAI model being used. This input will be passed to + :func:`tiktoken.encoding_for_model`. + + Returns + ------- + int + Total number of tokens in the set of messages outgoing to + OpenAI. + + References + ---------- + https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb + """ + message_total = sum( + ApiBase.count_tokens(message["content"], model=model) + 4 + for message in messages + ) + return message_total + 3 + + +class OpenAIService(RateLimitedService): + """OpenAI Chat GPT query service""" + + def __init__(self, client, rate_limit=1e3, rate_tracker=None): + """ + + Parameters + ---------- + client : openai.AsyncOpenAI | openai.AsyncAzureOpenAI + Async OpenAI client instance. Must have an async + `client.chat.completions.create` method. + rate_limit : int | float, optional + Token rate limit (typically per minute, but the time + interval is ultimately controlled by the `rate_tracker` + instance). By default, ``1e3``. + rate_tracker : TimeBoundedUsageTracker, optional + A TimeBoundedUsageTracker instance. This will be used to + track usage per time interval and compare to `rate_limit`. + If ``None``, a `TimeBoundedUsageTracker` instance is created + with default parameters. By default, ``None``. + """ + super().__init__(rate_limit, rate_tracker or TimeBoundedUsageTracker()) + self.client = client + + async def process( + self, usage_tracker=None, usage_sub_label="default", *, model, **kwargs + ): + """Process a call to OpenAI Chat GPT. + + Note that this method automatically retries queries (with + backoff) if a rate limit error is throw by the API. + + Parameters + ---------- + model : str + OpenAI GPT model to query. + usage_tracker : `elm.ords.services.usage.UsageTracker`, optional + UsageTracker instance. Providing this input will update your + tracker with this call's token usage info. + By default, ``None``. + usage_sub_label : str, optional + Optional label to categorize usage under. This can be used + to track usage related to certain categories. + By default, ``"default"``. + **kwargs + Keyword arguments to be passed to + `client.chat.completions.create`. + + Returns + ------- + str | None + Chat GPT response as a string, or ``None`` if the call + failed. + """ + self._record_prompt_tokens(model, kwargs) + response = await self._call_gpt(model=model, **kwargs) + self._record_completion_tokens(response) + self._record_usage(response, usage_tracker, usage_sub_label) + return _get_response_message(response) + + def _record_prompt_tokens(self, model, kwargs): + """Add prompt token count to rate tracker""" + num_tokens = count_tokens(kwargs.get("messages", []), model) + self.rate_tracker.add(num_tokens) + + def _record_usage(self, response, usage_tracker, usage_sub_label): + """Record token usage for user""" + if usage_tracker is None: + return + usage_tracker.update_from_model(response, sub_label=usage_sub_label) + + def _record_completion_tokens(self, response): + """Add completion token count to rate tracker""" + if response is None: + return + self.rate_tracker.add(response.usage.completion_tokens) + + @async_retry_with_exponential_backoff() + async def _call_gpt(self, **kwargs): + """Query Chat GPT with user inputs""" + try: + return await self.client.chat.completions.create(**kwargs) + except openai.BadRequestError as e: + logger.error("Got 'BadRequestError':") + logger.exception(e) + + +def _get_response_message(response): + """Get message as string from response object""" + if response is None: + return None + return response.choices[0].message.content diff --git a/elm/ords/services/provider.py b/elm/ords/services/provider.py new file mode 100644 index 00000000..df82fd1c --- /dev/null +++ b/elm/ords/services/provider.py @@ -0,0 +1,152 @@ +# -*- coding: utf-8 -*- +"""ELM service provider classes.""" +import asyncio +import logging + +from elm.ords.services.queues import ( + initialize_service_queue, + get_service_queue, + tear_down_service_queue, +) +from elm.ords.utilities.exceptions import ELMOrdsValueError + + +logger = logging.getLogger(__name__) + + +class _RunningProvider: + """A running provider for a single service.""" + + def __init__(self, service, queue): + """ + + Parameters + ---------- + service : :class:`elm.ords.services.base.Service` + An instance of a single async service to run. + queue : :class:`asyncio.Queue` + Queue object for the running service. + """ + self.service = service + self.queue = queue + self.jobs = set() + + async def run(self): + """Run the service.""" + while True: + await self.submit_jobs() + await self.collect_responses() + + async def submit_jobs(self): + """Submit jobs from the queue to processing. + + The service can limit the number of submissions at a time by + implementing the ``can_process`` property. + + If the queue is non-empty, the function takes jobs from it + iteratively and submits them until the ``can_process`` property + of the service returns ``False``. A call to ``can_process`` is + submitted between every job pulled from the queue, so enure that + method is performant. If the queue is empty, this function does + one of two things: + + 1) If there are no jobs processing, it waits on the queue + to get more jobs and submits them as they come in + (assuming the service allows it) + 2) If there are jobs processing, this function returns + without waiting on more jobs from the queue. + + """ + if not self.service.can_process or self._q_empty_but_still_processing: + return + + while self.service.can_process and self._can_fit_jobs: + fut, outer_task_name, args, kwargs = await self.queue.get() + task = asyncio.create_task( + self.service.process_using_futures(fut, *args, **kwargs), + name=outer_task_name, + ) + self.queue.task_done() + self.jobs.add(task) + await self._allow_service_to_update() + + return + + async def _allow_service_to_update(self): + """Switch contexts, allowing service to update if it can process""" + await asyncio.sleep(0) + + @property + def _q_empty_but_still_processing(self): + """bool: Queue empty but jobs still running (don't await queue)""" + return self.queue.empty() and self.jobs + + @property + def _can_fit_jobs(self): + """bool: Job tracker not full""" + return len(self.jobs) < self.service.MAX_CONCURRENT_JOBS + + async def collect_responses(self): + """Collect responses from the service. + + This call will block further submissions to the service until + at least one job finishes. + """ + if not self.jobs: + return + + complete, __ = await asyncio.wait( + self.jobs, return_when=asyncio.FIRST_COMPLETED + ) + + for job in complete: + self.jobs.remove(job) + + +class RunningAsyncServices: + """Async context manager for running services.""" + + def __init__(self, services): + """ + + Parameters + ---------- + services : iterable + An iterable of async services to run during program + execution. + """ + self.services = services + self.__providers = [] + self._validate_services() + + def _validate_services(self): + """Validate input services.""" + if len(self.services) < 1: + raise ELMOrdsValueError( + "Must provide at least one service to run!" + ) + + def _reset_providers(self): + """Reset running providers""" + for c in self.__providers: + c.cancel() + self.__providers = [] + + async def __aenter__(self): + for service in self.services: + logger.debug("Initializing Service: %s", service.name) + queue = initialize_service_queue(service.name) + service.acquire_resources() + task = asyncio.create_task(_RunningProvider(service, queue).run()) + self.__providers.append(task) + + async def __aexit__(self, exc_type, exc, tb): + try: + for service in self.services: + await get_service_queue(service.name).join() + service.release_resources() + finally: + self._reset_providers() + for service in self.services: + logger.debug("Tearing down Service: %s", service.name) + tear_down_service_queue(service.name) diff --git a/elm/ords/services/queues.py b/elm/ords/services/queues.py new file mode 100644 index 00000000..b2704341 --- /dev/null +++ b/elm/ords/services/queues.py @@ -0,0 +1,55 @@ +# -*- coding: utf-8 -*- +"""Module for "singleton" QUERIES dictionary""" +import asyncio + + +_QUEUES = {} + + +def initialize_service_queue(service_name): + """Initialize an `asyncio.Queue()` for a service. + + Repeated calls to this function return the same queue + + Parameters + ---------- + service_name : str + Name of service to initialize queue for. + + Returns + ------- + asyncio.Queue() + Queue instance for this service. + """ + return _QUEUES.setdefault(service_name, asyncio.Queue()) + + +def tear_down_service_queue(service_name): + """Remove the queue for a service. + + The queue does not have to exist, so repeated calls to this function + are OK. + + Parameters + ---------- + service_name : str + Name of service to delete queue for. + """ + _QUEUES.pop(service_name, None) + + +def get_service_queue(service_name): + """Retrieve the queue for a service. + + Parameters + ---------- + service_name : str + Name of service to retrieve queue for. + + Returns + ------- + asyncio.Queue() | None + Queue instance for this service, or `None` if the queue was not + initialized. + """ + return _QUEUES.get(service_name) diff --git a/elm/ords/services/threaded.py b/elm/ords/services/threaded.py new file mode 100644 index 00000000..cdf748b8 --- /dev/null +++ b/elm/ords/services/threaded.py @@ -0,0 +1,295 @@ +# -*- coding: utf-8 -*- +# pylint: disable=consider-using-with +"""ELM Ordinance Threaded services""" +import json +import shutil +import asyncio +from pathlib import Path +from functools import partial +from abc import abstractmethod +from tempfile import TemporaryDirectory +from concurrent.futures import ThreadPoolExecutor + +from elm.ords.services.base import Service +from elm.web.utilities import write_url_doc_to_file + + +def _move_file(doc, out_dir): + """Move a file from a temp directory to an output directory.""" + cached_fp = doc.metadata.get("cache_fn") + if cached_fp is None: + return + + cached_fp = Path(cached_fp) + out_fn = doc.metadata.get("location_name", cached_fp.name) + if not out_fn.endswith(cached_fp.suffix): + out_fn = f"{out_fn}{cached_fp.suffix}" + + out_fp = Path(out_dir) / out_fn + shutil.move(cached_fp, out_fp) + return out_fp + + +def _write_cleaned_file(doc, out_dir): + """Write cleaned ordinance text to directory.""" + cleaned_text = doc.metadata.get("cleaned_ordinance_text") + location_name = doc.metadata.get("location_name") + + if cleaned_text is None or location_name is None: + return + + out_fp = Path(out_dir) / f"{location_name} Summary.txt" + with open(out_fp, "w", encoding="utf-8") as fh: + fh.write(cleaned_text) + return out_fp + + +def _write_ord_db(doc, out_dir): + """Write parsed ordinance database to directory.""" + ord_db = doc.metadata.get("ordinance_values") + location_name = doc.metadata.get("location_name") + + if ord_db is None or location_name is None: + return + + out_fp = Path(out_dir) / f"{location_name} Ordinances.csv" + ord_db.to_csv(out_fp, index=False) + return out_fp + + +_PROCESSING_FUNCTIONS = { + "move": _move_file, + "write_clean": _write_cleaned_file, + "write_db": _write_ord_db, +} + + +class ThreadedService(Service): + """Service that contains a ThreadPoolExecutor instance""" + + def __init__(self, **kwargs): + """ + + Parameters + ---------- + **kwargs + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ThreadPoolExecutor`. + By default, ``None``. + """ + self._tpe_kwargs = kwargs or {} + self.pool = None + + def acquire_resources(self): + """Open thread pool and temp directory""" + self.pool = ThreadPoolExecutor(**self._tpe_kwargs) + + def release_resources(self): + """Shutdown thread pool and cleanup temp directory""" + self.pool.shutdown(wait=True, cancel_futures=True) + + +class TempFileCache(ThreadedService): + """Service that locally caches files downloaded from the internet""" + + def __init__(self, td_kwargs=None, tpe_kwargs=None): + """ + + Parameters + ---------- + td_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`tempfile.TemporaryDirectory`. By default, ``None``. + tpe_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ThreadPoolExecutor`. + By default, ``None``. + """ + super().__init__(**(tpe_kwargs or {})) + self._td_kwargs = td_kwargs or {} + self._td = None + + @property + def can_process(self): + """bool: Always ``True`` (limiting is handled by asyncio)""" + return True + + def acquire_resources(self): + """Open thread pool and temp directory""" + super().acquire_resources() + self._td = TemporaryDirectory(**self._td_kwargs) + + def release_resources(self): + """Shutdown thread pool and cleanup temp directory""" + self._td.cleanup() + super().release_resources() + + async def process(self, doc, file_content, make_name_unique=False): + """Write URL doc to file asynchronously. + + Parameters + ---------- + doc : elm.web.document.Document + Document containing meta information about the file. Must + have a "source" key in the `metadata` dict containing the + URL, which will be converted to a file name using + :func:`compute_fn_from_url`. + file_content : str | bytes + File content, typically string text for HTML files and bytes + for PDF file. + make_name_unique : bool, optional + Option to make file name unique by adding a UUID at the end + of the file name. By default, ``False``. + + Returns + ------- + Path + Path to output file. + """ + loop = asyncio.get_running_loop() + result = await loop.run_in_executor( + self.pool, + partial( + write_url_doc_to_file, + doc, + file_content, + self._td.name, + make_name_unique=make_name_unique, + ), + ) + return result + + +class StoreFileOnDisk(ThreadedService): + """Abstract service that manages the storage of a file on disk. + + Storage can occur due to creation or a move of a file. + """ + + def __init__(self, out_dir, tpe_kwargs=None): + """ + + Parameters + ---------- + out_dir : path-like + Path to output directory where file should be stored. + tpe_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ThreadPoolExecutor`. + By default, ``None``. + """ + super().__init__(**(tpe_kwargs or {})) + self.out_dir = out_dir + + @property + def can_process(self): + """bool: Always ``True`` (limiting is handled by asyncio)""" + return True + + async def process(self, doc): + """Store file in out directory. + + Parameters + ---------- + doc : elm.web.document.Document + Document containing meta information about the file. Must + have relevant processing keys in the `metadata` dict, + otherwise the file may not be stored in the output + directory. + + Returns + ------- + Path | None + Path to output file, or `None` if no file was stored. + """ + return await _run_func_in_pool( + self.pool, + partial(_PROCESSING_FUNCTIONS[self._PROCESS], doc, self.out_dir), + ) + + @property + @abstractmethod + def _PROCESS(self): + """str: Key in `_PROCESSING_FUNCTIONS` that defines the doc func.""" + raise NotImplementedError + + +class FileMover(StoreFileOnDisk): + """Service that moves files to an output directory""" + + _PROCESS = "move" + + +class CleanedFileWriter(StoreFileOnDisk): + """Service that writes cleaned text to a file""" + + _PROCESS = "write_clean" + + +class OrdDBFileWriter(StoreFileOnDisk): + """Service that writes cleaned text to a file""" + + _PROCESS = "write_db" + + +class UsageUpdater(ThreadedService): + """Service that updates usage info from a tracker into a file.""" + + def __init__(self, usage_fp, tpe_kwargs=None): + """ + + Parameters + ---------- + usage_fp : path-like + Path to JSON file where usage should be tracked. + tpe_kwargs : dict, optional + Keyword-value argument pairs to pass to + :class:`concurrent.futures.ThreadPoolExecutor`. + By default, ``None``. + """ + super().__init__(**(tpe_kwargs or {})) + self.usage_fp = usage_fp + self._is_processing = False + + @property + def can_process(self): + """bool: ``True`` if file not currently being written to.``""" + return not self._is_processing + + async def process(self, tracker): + """Add usage from tracker to file. + + Any existing usage info in the file will remain unchanged + EXCEPT for anything under the label of the input `tracker`, + all of which will be replaced with info from the tracker itself. + + Parameters + ---------- + tracker : elm.ods.services.usage.UsageTracker + A usage tracker instance that contains usage info to be + added to output file. + """ + self._is_processing = True + await _run_func_in_pool( + self.pool, partial(_dump_usage, self.usage_fp, tracker) + ) + self._is_processing = False + + +async def _run_func_in_pool(pool, callable_fn): + """Run a callable in process pool""" + loop = asyncio.get_running_loop() + return await loop.run_in_executor(pool, callable_fn) + + +def _dump_usage(fp, tracker): + """Dump usage to an existing file.""" + if not Path(fp).exists(): + usage_info = {} + else: + with open(fp, "r") as fh: + usage_info = json.load(fh) + + tracker.add_to(usage_info) + with open(fp, "w") as fh: + json.dump(usage_info, fh, indent=4) diff --git a/elm/ords/services/usage.py b/elm/ords/services/usage.py new file mode 100644 index 00000000..e811442f --- /dev/null +++ b/elm/ords/services/usage.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinances usage tracking utilities.""" +import time +import logging +from collections import UserDict, deque +from functools import total_ordering + + +logger = logging.getLogger(__name__) + + +@total_ordering +class TimedEntry: + """An entry that performs comparisons based on time added, not value. + + Examples + -------- + >>> a = TimedEntry(100) + >>> a > 1000 + True + """ + + def __init__(self, value): + """ + + Parameters + ---------- + value : obj + Some value to store as an entry. + """ + self.value = value + self._time = time.monotonic() + + def __eq__(self, other): + return self._time == other + + def __lt__(self, other): + return self._time < other + + +class TimeBoundedUsageTracker: + """Track usage of a resource over time. + + This class wraps a double-ended queue, and any inputs older than + a certain time are dropped. Those values are also subtracted from + the running total. + + References + ---------- + https://stackoverflow.com/questions/51485656/efficient-time-bound-queue-in-python + """ + + def __init__(self, max_seconds=70): + """ + + Parameters + ---------- + max_seconds : int, optional + Maximum age in seconds of an element before it is dropped + from consideration. By default, ``65``. + """ + self.max_seconds = max_seconds + self._total = 0 + self._q = deque() + + @property + def total(self): + """float: Total value of all entries younger than `max_seconds`""" + self._discard_old_values() + return self._total + + def add(self, value): + """Add a value to track. + + Parameters + ---------- + value : int | float + A new value to add to the queue. It's total will be added to + the running total, and it will live for `max_seconds` before + being discarded. + """ + self._q.append(TimedEntry(value)) + self._total += value + + def _discard_old_values(self): + """Discard 'old' values from the queue""" + cutoff_time = time.monotonic() - self.max_seconds + try: + while self._q[0] < cutoff_time: + self._total -= self._q.popleft().value + except IndexError: + pass + + +class UsageTracker(UserDict): + """Rate or AIP usage tracker.""" + + def __init__(self, label, response_parser): + """ + + Parameters + ---------- + label : str + Top-level label to use when adding this usage information to + another dictionary. + response_parser : callable + A callable that takes the current usage info (in dictionary + format) and an LLm response as inputs, updates the usage + dictionary with usage info based on the response, and + returns the updated dictionary. See, for example, + :func:`elm.ords.services.openai.usage_from_response`. + """ + super().__init__() + self.label = label + self.response_parser = response_parser + + def add_to(self, other): + """Add the contents of this usage information to another dict. + + The contents of this dictionary are stored under the `label` + key that this object was initialized with. + + Parameters + ---------- + other : dict + A dictionary to add the contents of this one to. + """ + other.update({self.label: {**self, "tracker_totals": self.totals}}) + + @property + def totals(self): + """Compute total usage across all sub-labels. + + Returns + ------- + dict + Dictionary containing usage information totaled across all + sub-labels. + """ + totals = {} + for report in self.values(): + try: + sub_label_report = report.items() + except AttributeError: + continue + + for tracked_value, count in sub_label_report: + totals[tracked_value] = totals.get(tracked_value, 0) + count + return totals + + def update_from_model(self, response=None, sub_label="default"): + """Update usage from a model response. + + Parameters + ---------- + response : object, optional + Model call response, which either contains usage information + or can be used to infer/compute usage. If ``None``, no + update is made. + sub_label : str, optional + Optional label to categorize usage under. This can be used + to track usage related to certain categories. + By default, ``"default"``. + """ + if response is None: + return + + self[sub_label] = self.response_parser( + self.get(sub_label, {}), response + ) diff --git a/elm/ords/utilities/__init__.py b/elm/ords/utilities/__init__.py new file mode 100644 index 00000000..12c9be41 --- /dev/null +++ b/elm/ords/utilities/__init__.py @@ -0,0 +1,21 @@ +"""ELM Ordinance utilities. """ + +from .counties import load_all_county_info, load_counties_from_fp +from .parsing import llm_response_as_json, merge_overlapping_texts + + +RTS_SEPARATORS = [ + "Setbacks", + "CHAPTER ", + "SECTION ", + "\r\n\r\n", + "\r\n", + "Chapter ", + "Section ", + "\n\n", + "\n", + "section ", + "chapter ", + " ", + "", +] diff --git a/elm/ords/utilities/counties.py b/elm/ords/utilities/counties.py new file mode 100644 index 00000000..c676eae2 --- /dev/null +++ b/elm/ords/utilities/counties.py @@ -0,0 +1,132 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance county info""" +import os +import logging +from warnings import warn + +import pandas as pd + +from elm import ELM_DIR +from elm.ords.utilities.exceptions import ELMOrdsValueError + + +logger = logging.getLogger(__name__) +_COUNTY_DATA_FP = os.path.join(ELM_DIR, "ords", "data", "conus_counties.csv") + + +def load_all_county_info(): + """Load DataFrame containing info like names and websites for all counties. + + Returns + ------- + pd.DataFrame + DataFrame containing county info like names, FIPS, websites, + etc. for all counties. + """ + county_info = pd.read_csv(_COUNTY_DATA_FP) + county_info = _convert_to_title(county_info, "County") + county_info = _convert_to_title(county_info, "State") + return county_info + + +def county_websites(county_info=None): + """Load mapping of county name and state to website. + + Parameters + ---------- + county_info : pd.DataFrame, optional + DataFrame containing county names and websites. If ``None``, + this info is loaded using :func:`load_county_info`. + By default, ``None``. + + Returns + ------- + dict + Dictionary where keys are tuples of (county, state) and keys are + the relevant website URL. Note that county and state names are + lowercase. + """ + if county_info is None: + county_info = load_all_county_info() + + return { + (row["County"].casefold(), row["State"].casefold()): row["Website"] + for __, row in county_info.iterrows() + } + + +def load_counties_from_fp(county_fp): + """Load county info base don counties in the input fp. + + Parameters + ---------- + county_fp : path-like + Path to csv file containing "County" and "State" columns that + define the counties for which info should be loaded. + + Returns + ------- + pd.DataFrame + DataFrame containing county info like names, FIPS, websites, + etc. for all requested counties (that were found). + """ + counties = pd.read_csv(county_fp) + _validate_county_input(counties) + + counties = _convert_to_title(counties, "County") + counties = _convert_to_title(counties, "State") + + all_county_info = load_all_county_info() + counties = counties.merge( + all_county_info, on=["County", "State"], how="left" + ) + + counties = _filter_not_found_counties(counties) + return _format_county_df_for_output(counties) + + +def _validate_county_input(df): + """Throw error if user is missing required columns""" + expected_cols = ["County", "State"] + missing = [col for col in expected_cols if col not in df] + if missing: + msg = ( + "The following required columns were not found in the county " + f"input: {missing}" + ) + raise ELMOrdsValueError(msg) + + +def _filter_not_found_counties(df): + """Filter out counties with null FIPS codes.""" + _warn_about_missing_counties(df) + return df[~df.FIPS.isna()].copy() + + +def _warn_about_missing_counties(df): + """Throw warning about counties that were not found in the main list.""" + not_found_counties = df[df.FIPS.isna()] + if len(not_found_counties): + not_found_counties_str = not_found_counties[ + ["County", "State"] + ].to_markdown(index=False, tablefmt="psql") + msg = ( + "The following counties were not found! Please make sure to " + "use proper spelling and capitalization.\n" + f"{not_found_counties_str}" + ) + logger.warning(msg) + warn(msg) + + +def _format_county_df_for_output(df): + """Format county DataFrame for output.""" + out_cols = ["County", "State", "County Type", "FIPS", "Website"] + df.FIPS = df.FIPS.astype(int) + return df[out_cols].reset_index(drop=True) + + +def _convert_to_title(df, column): + """Convert the values of a DataFrame column to titles.""" + df[column] = df[column].str.strip().str.casefold().str.title() + return df diff --git a/elm/ords/utilities/exceptions.py b/elm/ords/utilities/exceptions.py new file mode 100644 index 00000000..08dbfdad --- /dev/null +++ b/elm/ords/utilities/exceptions.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +"""Custom Exceptions and Errors for ELM Ordinances. """ +import logging + +from elm.exceptions import ELMError + + +logger = logging.getLogger("elm") + + +class ELMOrdsError(ELMError): + """Generic ELM Ordinance Error.""" + + def __init__(self, *args, **kwargs): + """Init exception and broadcast message to logger.""" + super().__init__(*args, **kwargs) + if args: + logger.error(str(args[0]), stacklevel=2) + + +class ELMOrdsNotInitializedError(ELMOrdsError): + """ELM Ordinances not initialized error.""" + + +class ELMOrdsValueError(ELMOrdsError, ValueError): + """ELM Ordinances ValueError.""" + + +class ELMOrdsRuntimeError(ELMOrdsError, RuntimeError): + """ELM Ordinances RuntimeError.""" diff --git a/elm/ords/utilities/location.py b/elm/ords/utilities/location.py new file mode 100644 index 00000000..7d8bdf04 --- /dev/null +++ b/elm/ords/utilities/location.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance location specification utilities""" +from abc import ABC, abstractmethod + + +class Location(ABC): + """Abstract location representation.""" + + def __init__(self, name): + """ + + Parameters + ---------- + name : str + Name of location. + """ + self.name = name + + @property + @abstractmethod + def full_name(self): + """str: Full name of location""" + + +class County(Location): + """Class representing a county""" + + def __init__(self, name, state, fips=None, is_parish=False): + """ + + Parameters + ---------- + name : str + Name of the county. + state : str + State containing the county. + fips : int | str, optional + Optional county FIPS code. By default, ``None``. + is_parish : bool, optional + Flag indicating wether or not this county is classified as + a parish. By default, ``False``. + """ + super().__init__(name) + self.state = state + self.fips = fips + self.is_parish = is_parish + + @property + def full_name(self): + """str: Full county name in format '{name} County, {state}'""" + loc_id = "Parish" if self.is_parish else "County" + return f"{self.name} {loc_id}, {self.state}" + + def __repr__(self): + return f"County({self.name}, {self.state}, is_parish={self.is_parish})" + + def __str__(self): + return self.full_name + + def __eq__(self, other): + if isinstance(other, self.__class__): + return ( + self.name.casefold() == other.name.casefold() + and self.state.casefold() == other.state.casefold() + and self.is_parish == other.is_parish + ) + if isinstance(other, str): + return ( + self.full_name.casefold() == other.casefold() + or f"{self.name}, {self.state}".casefold() == other.casefold() + ) + return False diff --git a/elm/ords/utilities/parsing.py b/elm/ords/utilities/parsing.py new file mode 100644 index 00000000..15f93d4c --- /dev/null +++ b/elm/ords/utilities/parsing.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinances parsing utilities.""" +import json +import logging + + +logger = logging.getLogger(__name__) + + +def llm_response_as_json(content): + """LLM response to JSON. + + Parameters + ---------- + content : str + LLM response that contains a string representation of + a JSON file. + + Returns + ------- + dict + Response parsed into dictionary. This dictionary will be empty + if the response cannot be parsed by JSON. + """ + content = content.lstrip().rstrip() + content = content.lstrip("```").lstrip("json").lstrip("\n") + content = content.rstrip("```") + content = content.replace("True", "true").replace("False", "false") + try: + content = json.loads(content) + except json.decoder.JSONDecodeError: + logger.error( + "LLM returned improperly formatted JSON. " + "This is likely due to the completion running out of tokens. " + "Setting a higher token limit may fix this error. " + "Also ensure you are requesting JSON output in your prompt. " + "JSON returned:\n%s", + content, + ) + content = {} + return content + + +# fmt: off +def merge_overlapping_texts(text_chunks, n=300): + """Merge chunks fo text by removing any overlap. + + Parameters + ---------- + text_chunks : iterable of str + Iterable containing text chunks which may or may not contain + consecutive overlapping portions. + n : int, optional + Number of characters to check at the beginning of each message + for overlap with the previous message. By default, ``100``. + + Returns + ------- + str + Merged text. + """ + if not text_chunks: + return "" + + out_text = text_chunks[0] + for next_text in text_chunks[1:]: + start_ind = out_text[-2 * n:].find(next_text[:n]) + if start_ind == -1: + out_text = "\n".join([out_text, next_text]) + continue + start_ind = 2 * n - start_ind + out_text = "".join([out_text, next_text[start_ind:]]) + return out_text diff --git a/elm/ords/utilities/queued_logging.py b/elm/ords/utilities/queued_logging.py new file mode 100644 index 00000000..32be009f --- /dev/null +++ b/elm/ords/utilities/queued_logging.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance queued logging. + +This module implements queued logging, mostly following this blog:" +https://www.zopatista.com/python/2019/05/11/asyncio-logging/ +""" +import asyncio +import logging +from pathlib import Path +from queue import SimpleQueue +from logging.handlers import QueueHandler, QueueListener + + +LOGGING_QUEUE = SimpleQueue() + + +class NoLocationFilter(logging.Filter): + """Filter that catches all records without a location attribute.""" + + def filter(self, record): + """Filter logging record. + + Parameters + ---------- + record : logging.LogRecord + Log record containing the log message + default attributes. + If the ``location`` attribute is missing or is a string in + the form "Task-XX", the filter returns ``True`` (i.e. record + is emitted). + + Returns + ------- + bool + If the record's ``location`` attribute is "missing". + """ + record_location = getattr(record, "location", None) + return record_location is None or "Task-" in record_location + + +class LocationFilter(logging.Filter): + """Filter down to logs from a coroutine processing a specific location.""" + + def __init__(self, location): + """ + + Parameters + ---------- + location : str + Location identifier. For example, ``"El Paso Colorado"``. + """ + self.location = location + + def filter(self, record): + """Filter logging record. + + Parameters + ---------- + record : logging.LogRecord + Log record containing the log message + default attributes. + Must have a ``location`` attribute that is a string + identifier, or this function will return ``False`` every + time. The ``location`` identifier will be checked against + the filter's location attribute to determine the output + result. + + Returns + ------- + bool + If the record's ``location`` attribute matches the filter's + ``location`` attribute. + """ + record_location = getattr(record, "location", None) + return record_location is not None and record_location == self.location + + +class LocalProcessQueueHandler(QueueHandler): + """QueueHandler that works within a single process (locally).""" + + def emit(self, record): + """Emit record with a location attribute equal to current asyncio task. + + Parameters + ---------- + record : logging.LogRecord + Log record containing the log message + default attributes. + This record will get a ``location`` attribute dynamically + added, with a value equal to the name of the current asyncio + task (i.e. ``asyncio.current_task().get_name()``). + """ + record.location = asyncio.current_task().get_name() + try: + self.enqueue(record) + except asyncio.CancelledError: + raise + except Exception: + self.handleError(record) + + +class LogListener: + """Class to listen to logging queue from coroutines and write to files.""" + + def __init__(self, logger_names, level="INFO"): + """ + + Parameters + ---------- + logger_names : iterable + An iterable of string, where each string is a logger name. + The logger corresponding to each of the names will be + equipped with a logging queue handler. + level : str, optional + Log level to set for each logger. By default, ``"INFO"``. + """ + self.logger_names = logger_names + self.level = level + self._listener = None + self._queue_handler = LocalProcessQueueHandler(LOGGING_QUEUE) + + def _setup_listener(self): + """Set up the queue listener""" + if self._listener is not None: + return + self._listener = QueueListener( + LOGGING_QUEUE, logging.NullHandler(), respect_handler_level=True + ) + self._listener.handlers = list(self._listener.handlers) + + def _add_queue_handler_to_loggers(self): + """Add a queue handler to each logger requested by user""" + for logger_name in self.logger_names: + logger = logging.getLogger(logger_name) + logger.addHandler(self._queue_handler) + logger.setLevel(self.level) + + def _remove_queue_handler_from_loggers(self): + """Remove the queue handler from each logger requested by user""" + for logger_name in self.logger_names: + logging.getLogger(logger_name).removeHandler(self._queue_handler) + + def _remove_all_handlers_from_listener(self): + """Remove all handlers still attached to listener.""" + if self._listener is None: + return + for handler in self._listener.handlers: + handler.close() + self._listener.handlers.remove(handler) + + def __enter__(self): + self._setup_listener() + self._add_queue_handler_to_loggers() + self._listener.start() + return self + + def __exit__(self, exc_type, exc, tb): + self._listener.stop() + self._remove_queue_handler_from_loggers() + self._remove_all_handlers_from_listener() + + async def __aenter__(self): + return self.__enter__() + + async def __aexit__(self, exc_type, exc, tb): + self.__exit__(exc_type, exc, tb) + + def addHandler(self, handler): + """Add a handler to the queue listener. + + Logs that are sent to the queue will be emitted to the handler. + + Parameters + ---------- + handler : logging.Handler + Log handler to parse log records. + """ + if handler not in self._listener.handlers: + self._listener.handlers.append(handler) + + def removeHandler(self, handler): + """Remove a handler from the queue listener. + + Logs that are sent to the queue will no longer be emitted to the + handler. + + Parameters + ---------- + handler : logging.Handler + Log handler to remove from queue listener. + """ + if handler in self._listener.handlers: + handler.close() + self._listener.handlers.remove(handler) + + +class LocationFileLog: + """Context manager to write logs for a location to a unique file.""" + + def __init__(self, listener, log_dir, location, level="INFO"): + """ + + Parameters + ---------- + listener : :class:`~elm.ords.utilities.queued_logging.LoggingListener` + A listener instance. The file handler will be added to this + listener. + log_dir : path-like + Path to output directory to contain log file. + location : str + Location identifier. For example, ``"El Paso Colorado"``. + This string will become part of the file name, so it must + contain only characters valid in a file name. + level : str, optional + Log level. By default, ``"INFO"``. + """ + self.log_dir = Path(log_dir) + self.location = location + self.level = level + self._handler = None + self._listener = listener + + def _create_log_dir(self): + """Create log output directory if it doesn't exist.""" + self.log_dir.mkdir(exist_ok=True, parents=True) + + def _setup_handler(self): + """Setup the file handler for this location.""" + self._handler = logging.FileHandler( + self.log_dir / f"{self.location}.log", encoding="utf-8" + ) + self._handler.setLevel(self.level) + self._handler.addFilter(LocationFilter(self.location)) + + def _break_down_handler(self): + """Tear down the file handler for this location.""" + if self._handler is None: + return + + self._handler.close() + self._handler = None + + def _add_handler_to_listener(self): + """Add the file handler for this location to the queue listener.""" + if self._handler is None: + raise ValueError("Must set up handler before listener!") + + self._listener.addHandler(self._handler) + + def _remove_handler_from_listener(self): + """Remove the file handler for this location from the listener.""" + if self._handler is None: + return + + self._listener.removeHandler(self._handler) + + def __enter__(self): + self._create_log_dir() + self._setup_handler() + self._add_handler_to_listener() + + def __exit__(self, exc_type, exc, tb): + self._remove_handler_from_listener() + self._break_down_handler() + + async def __aenter__(self): + self.__enter__() + + async def __aexit__(self, exc_type, exc, tb): + self.__exit__(exc_type, exc, tb) diff --git a/elm/ords/validation/__init__.py b/elm/ords/validation/__init__.py new file mode 100644 index 00000000..7bfc3c30 --- /dev/null +++ b/elm/ords/validation/__init__.py @@ -0,0 +1 @@ +"""ELM ordinance document content and source validation. """ diff --git a/elm/ords/validation/content.py b/elm/ords/validation/content.py new file mode 100644 index 00000000..4355579c --- /dev/null +++ b/elm/ords/validation/content.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance document content Validation logic + +These are primarily used to validate that a legal document applies to a +particular technology (e.g. Large Wind Energy Conversion Systems). +""" +import logging + + +logger = logging.getLogger(__name__) +NOT_WIND_WORDS = [ + "windy", + "winds", + "window", + "windiest", + "windbreak", + "windshield", + "wind blow", + "wind erosion", + "rewind", + "mini wecs", + "swecs", + "private wecs", + "pwecs", + "wind direction", + "wind movement", + "wind attribute", + "wind runway", + "wind load", + "wind orient", + "wind damage", +] +GOOD_WIND_KEYWORDS = ["wind", "setback"] +GOOD_WIND_ACRONYMS = ["wecs", "wes", "lwet", "uwet", "wef"] +_GOOD_ACRONYM_CONTEXTS = [ + " {acronym} ", + " {acronym}\n", + " {acronym}.", + "\n{acronym} ", + "\n{acronym}.", + "\n{acronym}\n", + "({acronym} ", + " {acronym})", +] +GOOD_WIND_PHRASES = ["wind energy conversion", "wind turbine", "wind tower"] + + +class ValidationWithMemory: + """Validate a set of text chunks by sometimes looking at previous chunks""" + + def __init__(self, structured_llm_caller, text_chunks, num_to_recall=2): + """ + + Parameters + ---------- + structured_llm_caller : elm.ords.llm.StructuredLLMCaller + StructuredLLMCaller instance. Used for structured validation + queries. + text_chunks : list of str + List of strings, each of which represent a chunk of text. + The order of the strings should be the order of the text + chunks. This validator may refer to previous text chunks to + answer validation questions. + num_to_recall : int, optional + Number of chunks to check for each validation call. This + includes the original chunk! For example, if + `num_to_recall=2`, the validator will first check the chunk + at the requested index, and then the previous chunk as well. + By default, ``2``. + """ + self.slc = structured_llm_caller + self.text_chunks = text_chunks + self.num_to_recall = num_to_recall + self.memory = [{} for _ in text_chunks] + + # fmt: off + def _inverted_mem(self, starting_ind): + """Inverted memory.""" + inverted_mem = self.memory[:starting_ind + 1:][::-1] + yield from inverted_mem[:self.num_to_recall] + + # fmt: off + def _inverted_text(self, starting_ind): + """Inverted text chunks""" + inverted_text = self.text_chunks[:starting_ind + 1:][::-1] + yield from inverted_text[:self.num_to_recall] + + async def parse_from_ind(self, ind, prompt, key): + """Validate a chunk of text. + + Validation occurs by querying the LLM using the input prompt and + parsing the `key` from the response JSON. The prompt should + request that the key be a boolean output. If the key retrieved + from the LLM response is False, a number of previous text chunks + are checked as well, using the same prompt. This can be helpful + in cases where the answer to the validation prompt (e.g. does + this text pertain to a large WECS?) is only found in a previous + text chunk. + + Parameters + ---------- + ind : int + Positive integer corresponding to the chunk index. + Must be less than `len(text_chunks)`. + prompt : str + Input LLM system prompt that describes the validation + question. This should request a JSON output from the LLM. + It should also take `key` as a formatting input. + key : str + A key expected in the JSON output of the LLM containing the + response for the validation question. This string will also + be used to format the system prompt before it is passed to + the LLM. + + Returns + ------- + bool + ``True`` if the LLM returned ``True`` for this text chunk or + `num_to_recall-1` text chunks before it. + ``False`` otherwise. + """ + logger.debug("Checking %r for ind %d", key, ind) + mem_text = zip(self._inverted_mem(ind), self._inverted_text(ind)) + for step, (mem, text) in enumerate(mem_text): + logger.debug("Mem at ind %d is %s", step, mem) + check = mem.get(key) + if check is None: + # logger.debug("text=%s", text) + content = await self.slc.call( + sys_msg=prompt.format(key=key), + content=text, + usage_sub_label="document_content_validation", + ) + check = mem[key] = content.get(key, False) + if check: + return check + return False + + +def possibly_mentions_wind(text, match_count_threshold=1): + """Perform a heuristic check for mention of wind energy in text. + + This check first strips the text of any wind "look-alike" words + (e.g. "window", "windshield", etc). Then, it checks for particular + keywords, acronyms, and phrases that pertain to wind in the text. + If enough keywords are mentions (as dictated by + `match_count_threshold`), this check returns ``True``. + + Parameters + ---------- + text : str + Input text that may or may not mention win in relation to wind + energy. + match_count_threshold : int, optional + Number of keywords that must match for the text to pass this + heuristic check. Count must be strictly greater than this value. + By default, ``1``. + + Returns + ------- + bool + ``True`` if the number of keywords/acronyms/phrases detected + exceeds the `match_count_threshold`. + """ + heuristics_text = _convert_to_heuristics_text(text) + total_keyword_matches = _count_single_keyword_matches(heuristics_text) + total_keyword_matches += _count_acronym_matches(heuristics_text) + total_keyword_matches += _count_phrase_matches(heuristics_text) + return total_keyword_matches > match_count_threshold + + +def _convert_to_heuristics_text(text): + """Convert text for heuristic wind content parsing""" + heuristics_text = text.casefold() + for word in NOT_WIND_WORDS: + heuristics_text = heuristics_text.replace(word, "") + return heuristics_text + + +def _count_single_keyword_matches(heuristics_text): + """Count number of good wind energy keywords that appear in text.""" + return sum(keyword in heuristics_text for keyword in GOOD_WIND_KEYWORDS) + + +def _count_acronym_matches(heuristics_text): + """Count number of good wind energy acronyms that appear in text.""" + acronym_matches = 0 + for context in _GOOD_ACRONYM_CONTEXTS: + acronym_keywords = { + context.format(acronym=acronym) for acronym in GOOD_WIND_ACRONYMS + } + acronym_matches = sum( + keyword in heuristics_text for keyword in acronym_keywords + ) + if acronym_matches > 0: + break + return acronym_matches + + +def _count_phrase_matches(heuristics_text): + """Count number of good wind energy phrases that appear in text.""" + return sum( + all(keyword in heuristics_text for keyword in phrase.split(" ")) + for phrase in GOOD_WIND_PHRASES + ) diff --git a/elm/ords/validation/location.py b/elm/ords/validation/location.py new file mode 100644 index 00000000..fd2cfdb9 --- /dev/null +++ b/elm/ords/validation/location.py @@ -0,0 +1,281 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance Location Validation logic + +These are primarily used to validate that a legal document applies to a +particular location. +""" +import asyncio +import logging +from abc import ABC, abstractmethod + +from elm.ords.extraction.ngrams import convert_text_to_sentence_ngrams + + +logger = logging.getLogger(__name__) + + +class FixedMessageValidator(ABC): + """Validation base class using a static system prompt.""" + + SYSTEM_MESSAGE = None + """LLM system message describing validation task. """ + + def __init__(self, structured_llm_caller): + """ + + Parameters + ---------- + structured_llm_caller : :class:`elm.ords.llm.StructuredLLMCaller` + StructuredLLMCaller instance. Used for structured validation + queries. + """ + self.slc = structured_llm_caller + + async def check(self, content, **fmt_kwargs): + """Check if the content passes the validation. + + The exact validation is outlined in the class `SYSTEM_MESSAGE`. + + Parameters + ---------- + content : str + Document content to validate. + **fmt_kwargs + Keyword arguments to be passed to `SYSTEM_MESSAGE.format()`. + + Returns + ------- + bool + ``True`` if the content passes the validation check, + ``False`` otherwise. + """ + if not content: + return False + sys_msg = self.SYSTEM_MESSAGE.format(**fmt_kwargs) + out = await self.slc.call( + sys_msg, content, usage_sub_label="document_location_validation" + ) + return self._parse_output(out) + + @abstractmethod + def _parse_output(self, props): + """Parse LLM response and return `True` if the document passes.""" + raise NotImplementedError + + +class URLValidator(FixedMessageValidator): + """Validator that checks wether a URL matches a county.""" + + SYSTEM_MESSAGE = ( + "You extract structured data from a URL. Return your " + "answer in JSON format. Your JSON file must include exactly two keys. " + "The first key is 'correct_county', which is a boolean that is set to " + "`True` if the URL mentions {county} County in some way. DO NOT infer " + "based on information in the URL about any US state, city, township, " + "or otherwise. `False` if not sure. The second key is " + "'correct_state', which is a boolean that is set to `True` if the URL " + "mentions {state} State in some way. DO NOT infer based on " + "information in the URL about any US county, city, township, or " + "otherwise. `False` if not sure." + ) + + def _parse_output(self, props): + """Parse LLM response and return `True` if the document passes.""" + logger.debug("Parsing URL validation output:\n\t%s", props) + check_vars = ("correct_county", "correct_state") + return all(props.get(var) for var in check_vars) + + +class CountyJurisdictionValidator(FixedMessageValidator): + """Validator that checks wether text applies at the county level.""" + + SYSTEM_MESSAGE = ( + "You extract structured data from legal text. Return " + "your answer in JSON format. Your JSON file must include exactly " + "three keys. The first key is 'x', which is a boolean that is set to " + "`True` if the text excerpt explicitly mentions that the regulations " + "within apply to a jurisdiction scope other than {county} County " + "(i.e. they apply to a subdivision like a township or a city, or " + "they apply more broadly, like to a state or the full country). " + "`False` if the regulations in the text apply at the {county} County " + "level, if the regulations in the text apply to all unincorporated " + "areas of {county} County, or if there is not enough information to " + "determine the answer. The second key is 'y', which is a boolean " + "that is set to `True` if the text excerpt explicitly mentions that " + "the regulations within apply to more than one county. `False` if " + "the regulations in the text excerpt apply to a single county only " + "or if there is not enough information to determine the answer. The " + "third key is 'explanation', which is a string that contains a short " + "explanation if you chose `True` for any answers above." + ) + + def _parse_output(self, props): + """Parse LLM response and return `True` if the document passes.""" + logger.debug( + "Parsing county jurisdiction validation output:\n\t%s", props + ) + check_vars = ("x", "y") + return not any(props.get(var) for var in check_vars) + + +class CountyNameValidator(FixedMessageValidator): + """Validator that checks wether text applies to a particular county.""" + + SYSTEM_MESSAGE = ( + "You extract structured data from legal text. Return " + "your answer in JSON format. Your JSON file must include exactly " + "three keys. The first key is 'wrong_county', which is a boolean that " + "is set to `True` if the legal text is not for {county} County. Do " + "not infer based on any information about any US state, city, " + "township, or otherwise. `False` if the text applies to {county} " + "County or if there is not enough information to determine the " + "answer. The second key is 'wrong_state', which is a boolean that is " + "set to `True` if the legal text is not for a county in {state} " + "State. Do not infer based on any information about any US county, " + "city, township, or otherwise. `False` if the text applies to " + "a county in {state} State or if there is not enough information to " + "determine the answer. The third key is 'explanation', which is a " + "string that contains a short explanation if you chose `True` for " + "any answers above." + ) + + def _parse_output(self, props): + """Parse LLM response and return `True` if the document passes.""" + logger.debug("Parsing county validation output:\n\t%s", props) + check_vars = ("wrong_county", "wrong_state") + return not any(props.get(var) for var in check_vars) + + +class CountyValidator: + """ELM Ords County validator. + + Combines the logic of several validators into a single class. + """ + + def __init__(self, structured_llm_caller, score_thresh=0.8): + """ + + Parameters + ---------- + structured_llm_caller : :class:`elm.ords.llm.StructuredLLMCaller` + StructuredLLMCaller instance. Used for structured validation + queries. + score_thresh : float, optional + Score threshold to exceed when voting on content from raw + pages. By default, ``0.8``. + """ + self.score_thresh = score_thresh + self.cn_validator = CountyNameValidator(structured_llm_caller) + self.cj_validator = CountyJurisdictionValidator(structured_llm_caller) + self.url_validator = URLValidator(structured_llm_caller) + + async def check(self, doc, county, state): + """Check if the document belongs to the county. + + Parameters + ---------- + doc : :class:`elm.web.document.BaseDocument` + Document instance. Should contain a "source" key in the + metadata that contains a URL (used for the URL validation + check). Raw content will be parsed for county name and + correct jurisdiction. + county : str + County that document should belong to. + state : str + State corresponding to `county` input. + + Returns + ------- + bool + `True` if the doc contents pertain to the input county. + `False` otherwise. + """ + source = doc.metadata.get("source") + logger.debug( + "Validating document from source: %s", source or "Unknown" + ) + logger.debug("Checking for correct for jurisdiction...") + jurisdiction_is_county = await _validator_check_for_doc( + validator=self.cj_validator, + doc=doc, + score_thresh=self.score_thresh, + county=county, + ) + if not jurisdiction_is_county: + return False + + logger.debug( + "Checking URL (%s) for county name...", source or "Unknown" + ) + url_is_county = await self.url_validator.check( + source, county=county, state=state + ) + if url_is_county: + return True + + logger.debug( + "Checking text for county name (heuristic; URL: %s)...", + source or "Unknown", + ) + correct_county_heuristic = _heuristic_check_for_county_and_state( + doc, county, state + ) + logger.debug( + "Found county name in text (heuristic): %s", + correct_county_heuristic, + ) + if correct_county_heuristic: + return True + + logger.debug( + "Checking text for county name (LLM; URL: %s)...", + source or "Unknown", + ) + return await _validator_check_for_doc( + validator=self.cn_validator, + doc=doc, + score_thresh=self.score_thresh, + county=county, + state=state, + ) + + +def _heuristic_check_for_county_and_state(doc, county, state): + """Check if county and state names are in doc""" + return any( + any( + (county.lower() in fg and state.lower() in fg) + for fg in convert_text_to_sentence_ngrams(t.lower(), 5) + ) + for t in doc.pages + ) + + +async def _validator_check_for_doc(validator, doc, score_thresh=0.8, **kwargs): + """Apply a validator check to a doc's raw pages.""" + outer_task_name = asyncio.current_task().get_name() + validation_checks = [ + asyncio.create_task( + validator.check(text, **kwargs), name=outer_task_name + ) + for text in doc.raw_pages + ] + out = await asyncio.gather(*validation_checks) + score = _weighted_vote(out, doc) + logger.debug( + "%s score is %.2f for doc from source %s (Pass: %s)", + validator.__class__.__name__, + score, + doc.metadata.get("source", "Unknown"), + str(score > score_thresh), + ) + return score > score_thresh + + +def _weighted_vote(out, doc): + """Compute weighted average of responses based on text length.""" + if not doc.raw_pages: + return 0 + weights = [len(text) for text in doc.raw_pages] + total = sum(verdict * weight for verdict, weight in zip(out, weights)) + return total / sum(weights) diff --git a/elm/pdf.py b/elm/pdf.py index ac1fcfba..40ad048c 100644 --- a/elm/pdf.py +++ b/elm/pdf.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- +# fmt: off """ ELM PDF to text parser """ import os import subprocess -import numpy as np import requests import tempfile import copy @@ -12,6 +12,7 @@ import logging from elm.base import ApiBase +from elm.utilities.parse import is_multi_col, combine_pages, clean_headers logger = logging.getLogger(__name__) @@ -49,7 +50,7 @@ def __init__(self, fp, page_range=None, model=None): self.fp = fp self.raw_pages = self.load_pdf(page_range) self.pages = self.raw_pages - self.full = self.combine_pages(self.raw_pages) + self.full = combine_pages(self.raw_pages) def load_pdf(self, page_range): """Basic load of pdf to text strings @@ -148,7 +149,7 @@ def clean_txt(self): logger.info('Finished cleaning PDF.') self.pages = clean_pages - self.full = self.combine_pages(self.pages) + self.full = combine_pages(self.pages) self.validate_clean() return clean_pages @@ -200,7 +201,7 @@ async def clean_txt_async(self, ignore_error=None, rate_limit=40e3): logger.info('Finished cleaning PDF.') self.pages = clean_pages - self.full = self.combine_pages(self.pages) + self.full = combine_pages(self.pages) self.validate_clean() return clean_pages @@ -218,12 +219,7 @@ def is_double_col(self, separator=' '): out : bool True if more than one vertical text column """ - lines = self.full.split('\n') - n_cols = np.zeros(len(lines)) - for i, line in enumerate(lines): - columns = line.strip().split(separator) - n_cols[i] = len(columns) - return np.median(n_cols) >= 2 + return is_multi_col(self.full, separator=separator) def clean_poppler(self, layout=True): """Clean the pdf using the poppler pdftotxt utility @@ -283,7 +279,7 @@ def clean_poppler(self, layout=True): for i in remove[::-1]: _ = self.pages.pop(i) - self.full = self.combine_pages(self.pages) + self.full = combine_pages(self.pages) return self.full @@ -325,55 +321,6 @@ def replace_chars_for_clean(text): .format(i + 1, len(self.raw_pages), perc, len(raw_words))) - @staticmethod - def combine_pages(pages): - """Combine pages of GPT cleaned text into a single string. - - Parameters - ---------- - pages : list - List of clean text strings where each list entry is a page from the - PDF - - Returns - ------- - full : str - Single multi-page string - """ - full = '\n'.join(pages) - full = full.replace('\n•', '-') - full = full.replace('•', '-') - return full - - def _get_nominal_headers(self, split_on, iheaders): - """Get nominal headers from a standard page. Aim for a "typical" page - that is likely to have a normal header, not the first or last. - - Parameters - ---------- - split_on : str - Chars to split lines of a page on - iheaders : list | tuple - Integer indices to look for headers after splitting a page into - lines based on split_on. This needs to go from the start of the - page to the end. - - Returns - ------- - headers : list - List of headers where each entry is a string header - """ - - headers = [None] * len(iheaders) - page_lens = np.array([len(p) for p in self.pages]) - median_len = np.median(page_lens) - ipage = np.argmin(np.abs(page_lens - median_len)) - page = self.pages[ipage] - for i, ih in enumerate(iheaders): - headers[i] = page.split(split_on)[ih] - - return headers - def clean_headers(self, char_thresh=0.6, page_thresh=0.8, split_on='\n', iheaders=(0, 1, -2, -1)): """Clean headers/footers that are duplicated across pages @@ -398,49 +345,8 @@ def clean_headers(self, char_thresh=0.6, page_thresh=0.8, split_on='\n', out : str Clean text with all pages joined """ - logger.info('Cleaning headers') - headers = self._get_nominal_headers(split_on, iheaders) - tests = np.zeros((len(self.pages), len(headers))) - - for ip, page in enumerate(self.pages): - for ih, header in zip(iheaders, headers): - pheader = '' - try: - pheader = page.split(split_on)[ih] - except IndexError: - pass - - harr = header.replace(' ', '') - parr = pheader.replace(' ', '') - - harr = harr.ljust(len(parr)) - parr = parr.ljust(len(harr)) - - harr = np.array([*harr]) - parr = np.array([*parr]) - assert len(harr) == len(parr) - - test = harr == parr - if len(test) == 0: - test = 1.0 - else: - test = test.sum() / len(test) - - tests[ip, ih] = test - - logger.debug('Header tests (page, iheader): \n{}'.format(tests)) - tests = (tests > char_thresh).sum(axis=0) / len(self.pages) - tests = (tests > page_thresh) - logger.debug('Header tests (iheader,): \n{}'.format(tests)) - - for ip, page in enumerate(self.pages): - page = page.split(split_on) - for i, iheader in enumerate(iheaders): - if tests[i] and len(page) > np.abs(iheader): - _ = page.pop(iheader) - - page = split_on.join(page) - self.pages[ip] = page - - self.full = self.combine_pages(self.pages) + self.pages = clean_headers(self.pages, char_thresh=char_thresh, + page_thresh=page_thresh, split_on=split_on, + iheaders=iheaders) + self.full = combine_pages(self.pages) return self.full diff --git a/elm/tree.py b/elm/tree.py index 8946ac08..d2ea49d5 100644 --- a/elm/tree.py +++ b/elm/tree.py @@ -126,14 +126,20 @@ def call_node(self, node0): out : str Next node or LLM response if at a leaf node. """ + prompt = self._prepare_graph_call(node0) + out = self.api.chat(prompt) + return self._parse_graph_output(node0, out) + def _prepare_graph_call(self, node0): + """Prepare a graph call for given node.""" prompt = self.graph.nodes[node0]['prompt'] txt_fmt = {k: v for k, v in self.graph.graph.items() if k != 'api'} prompt = prompt.format(**txt_fmt) - self._history.append(node0) - out = self.api.chat(prompt) + return prompt + def _parse_graph_output(self, node0, out): + """Parse graph output for given node and LLM call output. """ successors = list(self.graph.successors(node0)) edges = [self.graph.edges[(node0, node1)] for node1 in successors] conditions = [edge.get('condition', None) for edge in edges] diff --git a/elm/utilities/__init__.py b/elm/utilities/__init__.py new file mode 100644 index 00000000..f637dab9 --- /dev/null +++ b/elm/utilities/__init__.py @@ -0,0 +1 @@ +"""ELM utility classes and functions. """ diff --git a/elm/utilities/parse.py b/elm/utilities/parse.py new file mode 100644 index 00000000..a01db0c5 --- /dev/null +++ b/elm/utilities/parse.py @@ -0,0 +1,462 @@ +# -*- coding: utf-8 -*- +"""ELM parsing utilities.""" +import io +import re +import logging +from warnings import warn + +import html2text +import numpy as np +import pandas as pd + + +logger = logging.getLogger(__name__) + + +def is_multi_col(text, separator=" "): + """Does the text look like it has multiple vertical text columns? + + Parameters + ---------- + text : str + Input text, which may or may not contain multiple vertical + columns. + separator : str + Heuristic split string to look for spaces between columns + + Returns + ------- + out : bool + True if more than one vertical text column + """ + n_cols = [len(line.strip().split(separator)) for line in text.split("\n")] + return np.median(n_cols) >= 2 + + +def remove_blank_pages(pages): + """Remove any blank pages from the iterable. + + Parameters + ---------- + pages : iterable + Iterable of string objects. Objects in this iterable that do not + contain any text will be removed. + + Returns + ------- + list + List of strings with content, or empty list. + """ + return [page for page in pages if any(page.strip())] + + +def html_to_text(html, ignore_links=True): + """Call to `HTML2Text` class with basic args. + + Parameters + ---------- + html : str + HTML text extracted from the web. + ignore_links : bool, optional + Option to ignore links in HTML when parsing. + By default, ``True``. + + Returns + ------- + str + Text extracted from the input HTML. + """ + h = html2text.HTML2Text() + h.ignore_links = ignore_links + h.ignore_images = True + h.bypass_tables = True + return h.handle(html) + + +def format_html_tables(text, **kwargs): + """Format tables within HTML text into pretty markdown. + + Note that if pandas does not detect enough tables in the text to + match the "" tags, no replacement is performed at all. + + Parameters + ---------- + text : str + HTML text, possible containing tables enclosed by the + "
" tag. + **kwargs + Keyword-arguments to pass to ``pandas.DataFrame.to_markdown`` + function. Must not contain the `"headers"` keyword (this is + supplied internally). + + Returns + ------- + str + Text with HTML tables (if any) converted to markdown. + """ + matches = _find_html_table_matches(text) + if not matches: + return text + + dfs = _find_dfs(text) + if len(matches) != len(dfs): + logger.error( + "Found incompatible number of HTML (%d) and parsed (%d) tables! " + "No replacement performed.", + len(matches), + len(dfs), + ) + return text + + return _replace_tables_in_text(text, matches, dfs, **kwargs) + + +def _find_html_table_matches(text): + """Find HTML table matches in the text""" + return re.findall(r"
[\s\S]*?
", text) + + +def _find_dfs(text): + """Load HTML tables from text into DataFrames""" + return pd.read_html(io.StringIO(text)) + + +def _replace_tables_in_text(text, matches, dfs, **kwargs): + """Replace all items in the 'matches' input with MD tables""" + for table_str, df in zip(matches, dfs): + new_table_str = df.to_markdown(headers=df.columns, **kwargs) + text = text.replace(table_str, new_table_str) + return text + + +def clean_headers( + pages, + char_thresh=0.6, + page_thresh=0.8, + split_on="\n", + iheaders=(0, 1, -2, -1), +): + """Clean headers/footers that are duplicated across pages of a document. + + Note that this function will update the items within the `pages` + input. + + Parameters + ---------- + pages : list + List of pages (as str) from document. + char_thresh : float + Fraction of characters in a given header that are similar + between pages to be considered for removal + page_thresh : float + Fraction of pages that share the header to be considered for + removal + split_on : str + Chars to split lines of a page on + iheaders : list | tuple + Integer indices to look for headers after splitting a page into + lines based on split_on. This needs to go from the start of the + page to the end. + + Returns + ------- + out : str + Clean text with all pages joined + """ + logger.info("Cleaning headers") + headers = _get_nominal_headers(pages, split_on, iheaders) + tests = np.zeros((len(pages), len(headers))) + + for ip, page in enumerate(pages): + for ih, header in zip(iheaders, headers): + pheader = "" + try: + pheader = page.split(split_on)[ih] + except IndexError: + pass + + harr = header.replace(" ", "") + parr = pheader.replace(" ", "") + + harr = harr.ljust(len(parr)) + parr = parr.ljust(len(harr)) + + harr = np.array([*harr]) + parr = np.array([*parr]) + assert len(harr) == len(parr) + + test = harr == parr + if len(test) == 0: + test = 1.0 + else: + test = test.sum() / len(test) + + tests[ip, ih] = test + + logger.debug("Header tests (page, iheader): \n{}".format(tests)) + tests = (tests > char_thresh).sum(axis=0) / len(pages) + tests = tests > page_thresh + logger.debug("Header tests (iheader,): \n{}".format(tests)) + + header_inds_to_remove = { + ind for is_header, ind in zip(tests, iheaders) if is_header + } + if not header_inds_to_remove: + return pages + + for ip, page in enumerate(pages): + page = page.split(split_on) + if len(iheaders) >= len(page): + continue + pages[ip] = split_on.join( + [ + line + for line_ind, line in enumerate(page) + if line_ind not in header_inds_to_remove + and line_ind - len(page) not in header_inds_to_remove + ] + ) + + return pages + + +def _get_nominal_headers(pages, split_on, iheaders): + """Get nominal headers from a standard page. + + This function aims for a "typical" page that is likely to have a + normal header, not the first or last. + + Parameters + ---------- + pages : list + List of pages (as str) from document. + split_on : str + Chars to split lines of a page on + iheaders : list | tuple + Integer indices to look for headers after splitting a page into + lines based on split_on. This needs to go from the start of the + page to the end. + + Returns + ------- + headers : list + List of headers where each entry is a string header + """ + + headers = [None] * len(iheaders) + page_lens = np.array([len(p) for p in pages]) + median_len = np.median(page_lens) + ipage = np.argmin(np.abs(page_lens - median_len)) + page = pages[ipage] + for i, ih in enumerate(iheaders): + try: + header = page.split(split_on)[ih] + except IndexError: + header = "" + headers[i] = header + + return headers + + +def combine_pages(pages): + """Combine pages of GPT cleaned text into a single string. + + Parameters + ---------- + pages : list + List of pages (as str) from document. + + Returns + ------- + full : str + Single multi-page string + """ + return "\n".join(pages).replace("\n•", "-").replace("•", "-") + + +def replace_common_pdf_conversion_chars(text): + """Re-format text to remove common pdf-converter chars. + + Chars affected include ``\\r\\n``, ``\\r`` and ``\\x0c``. + + Parameters + ---------- + text : str + Input text (presumably from pdf parser). + + Returns + ------- + str + Cleaned text. + """ + return text.replace("\r\n", "\n").replace("\x0c", "").replace("\r", "\n") + + +def replace_multi_dot_lines(text): + """Replace instances of three or more dots (.....) with just "..." + + Parameters + ---------- + text : str + Text possibly containing many repeated dots. + + Returns + ------- + str + Cleaned text with only three dots max in a row. + """ + return re.sub(r"[.]{3,}", "...", text) + + +def replace_excessive_newlines(text): + """Replace instances of three or more newlines with ``\\n\\n`` + + Parameters + ---------- + text : str + Text possibly containing many repeated newline characters. + + Returns + ------- + str + Cleaned text with only a maximum of two newlines in a row. + """ + return re.sub(r"[\n]{3,}", "\n\n", text) + + +def remove_empty_lines_or_page_footers(text): + """Replace empty lines (potentially with page numbers only) as newlines + + Parameters + ---------- + text : str + Text possibly containing empty lines and/or lines with only page + numbers. + + Returns + ------- + str + Cleaned text with no empty lines. + """ + return re.sub(r"[\n\r]+(?:\s*?\d*?\s*)[\n\r]+", "\n", text) + + +def read_pdf(pdf_bytes, verbose=True): + """Read PDF contents from bytes. + + This method will automatically try to detect multi-column format + and load the text without a physical layout in that case. + + Parameters + ---------- + pdf_bytes : bytes + Bytes corresponding to a PDF file. + verbose : bool, optional + Option to log errors during parsing. By default, ``True``. + + Returns + ------- + iterable + Iterable containing pages of the PDF document. This iterable + may be empty if there was an error reading the PDF file. + """ + import pdftotext + + try: + pages = _load_pdf_possibly_multi_col(pdf_bytes) + except pdftotext.Error as e: + if verbose: + logger.error("Failed to decode PDF content!") + logger.exception(e) + pages = [] + + return pages + + +def _load_pdf_possibly_multi_col(pdf_bytes): + """Load PDF, which may be multi-column""" + import pdftotext + + pdf_bytes = io.BytesIO(pdf_bytes) + pages = pdftotext.PDF(pdf_bytes, physical=True) + if is_multi_col(combine_pages(pages)): + pages = pdftotext.PDF(pdf_bytes, physical=False) + return pages + + +def read_pdf_ocr(pdf_bytes, verbose=True): # pragma: no cover + """Read PDF contents from bytes using Optical Character recognition (OCR). + + This method attempt to read the PDF document using OCR. This is one + of the only ways to parse a scanned PDF document. To use this + function, you will need to install the `pytesseract` and `pdf2image` + Modules. Installation guides here: + + - `pytesseract`: + https://github.com/madmaze/pytesseract?tab=readme-ov-file#installation + - `pdf2image`: + https://github.com/Belval/pdf2image?tab=readme-ov-file#how-to-install + + Windows users may also need to apply the fix described in this + answer before they can use pytesseract: http://tinyurl.com/v9xr4vrj + + Parameters + ---------- + pdf_bytes : bytes + Bytes corresponding to a PDF file. + verbose : bool, optional + Option to log errors during parsing. By default, ``True``. + + Returns + ------- + iterable + Iterable containing pages of the PDF document. This iterable + may be empty if there was an error reading the PDF file. + """ + try: + pages = _load_pdf_with_pytesseract(pdf_bytes) + except Exception as e: + if verbose: + logger.error("Failed to decode PDF content!") + logger.exception(e) + pages = [] + + return pages + + +def _load_pdf_with_pytesseract(pdf_bytes): # pragma: no cover + """Load PDF bytes using Optical Character recognition (OCR)""" + + try: + import pytesseract + except ImportError: + msg = ( + "Module `pytesseract` not found. Please follow these instructions " + "to install: https://github.com/madmaze/pytesseract?" + "tab=readme-ov-file#installation" + ) + logger.warning(msg) + warn(msg) + return [] + + try: + from pdf2image import convert_from_bytes + except ImportError: + msg = ( + "Module `pdf2image` not found. Please follow these instructions " + "to install: https://github.com/Belval/pdf2image?" + "tab=readme-ov-file#how-to-install" + ) + logger.warning(msg) + warn(msg) + return [] + + logger.debug( + "Loading PDF with `tesseract_cmd` as %s", + pytesseract.pytesseract.tesseract_cmd, + ) + + return [ + str(pytesseract.image_to_string(page_data).encode("utf-8")) + for page_data in convert_from_bytes(bytes(pdf_bytes)) + ] diff --git a/elm/utilities/retry.py b/elm/utilities/retry.py new file mode 100644 index 00000000..8e1f9832 --- /dev/null +++ b/elm/utilities/retry.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +"""ELM retry utilities.""" +import time +import random +import asyncio +import logging +from functools import wraps + +import openai + +from elm.exceptions import ELMRuntimeError + + +logger = logging.getLogger(__name__) + + +def retry_with_exponential_backoff( + base_delay=1, + exponential_base=4, + jitter=True, + max_retries=3, + errors=(openai.RateLimitError, openai.APITimeoutError), +): + """Retry a synchronous function with exponential backoff. + + This decorator works out-of-the-box for OpenAI chat completions + calls. To configure it for other functions, set the `errors` input + accordingly. + + Parameters + ---------- + base_delay : int, optional + The base delay time, in seconds. This time will be multiplied by + the exponential_base (plus any jitter) during each retry + iteration. The multiplication applies *at the first retry*. + Therefore, if your base delay is ``1`` and your + `exponential_base` is ``4`` (with no jitter), the delay before + the first retry will be ``1 * 4 = 4`` seconds. The subsequent + delay will be ``4 * 4 = 16`` seconds, and so on. + By default, ``1``. + exponential_base : int, optional + The multiplication factor applied to the base `delay` input. + See description of `delay` for an example. By default, ``4``. + jitter : bool, optional + Option to include a random fractional adder (0 - 1) to the + `exponential_base` before multiplying by the `delay`. This can + help ensure each function call is submitted slightly offset from + other calls in a batch and therefore help avoid repeated rate + limit failures by a batch of submissions arriving simultaneously + to a service. By default, ``True``. + max_retries : int, optional + Max number of retries before raising an `ELMRuntimeError`. + By default, ``3``. + errors : tuple, optional + The error class(es) to signal a retry. Other errors will be + propagated without retrying. + By default, ``(openai.RateLimitError, openai.APITimeoutError)``. + + References + ---------- + https://github.com/openai/openai-cookbook/blob/main/examples/How_to_handle_rate_limits.ipynb + https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + """ + + def decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + num_retries = 0 + delay = base_delay + + while True: + try: + return func(*args, **kwargs) + except errors as e: + num_retries = _handle_retries(num_retries, max_retries, e) + delay = _compute_delay(delay, exponential_base, jitter) + logger.info( + "Error: %s. Retrying in %.2f seconds.", str(e), delay + ) + kwargs = _double_timeout(**kwargs) + time.sleep(delay) + + return wrapper + + return decorator + + +def async_retry_with_exponential_backoff( + base_delay=1, + exponential_base=4, + jitter=True, + max_retries=3, + errors=(openai.RateLimitError, openai.APITimeoutError), +): + """Retry an asynchronous function with exponential backoff. + + This decorator works out-of-the-box for OpenAI chat completions + calls. To configure it for other functions, set the `errors` input + accordingly. + + Parameters + ---------- + base_delay : int, optional + The base delay time, in seconds. This time will be multiplied by + the exponential_base (plus any jitter) during each retry + iteration. The multiplication applies *at the first retry*. + Therefore, if your base delay is ``1`` and your + `exponential_base` is ``4`` (with no jitter), the delay before + the first retry will be ``1 * 4 = 4`` seconds. The subsequent + delay will be ``4 * 4 = 16`` seconds, and so on. + By default, ``1``. + exponential_base : int, optional + The multiplication factor applied to the base `delay` input. + See description of `delay` for an example. By default, ``4``. + jitter : bool, optional + Option to include a random fractional adder (0 - 1) to the + `exponential_base` before multiplying by the `delay`. This can + help ensure each function call is submitted slightly offset from + other calls in a batch and therefore help avoid repeated rate + limit failures by a batch of submissions arriving simultaneously + to a service. By default, ``True``. + max_retries : int, optional + Max number of retries before raising an `ELMRuntimeError`. + By default, ``3``. + errors : tuple, optional + The error class(es) to signal a retry. Other errors will be + propagated without retrying. + By default, ``(openai.RateLimitError, openai.APITimeoutError)``. + + References + ---------- + https://github.com/openai/openai-cookbook/blob/main/examples/How_to_handle_rate_limits.ipynb + https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + """ + + def decorator(func): + @wraps(func) + async def wrapper(*args, **kwargs): + num_retries = 0 + delay = base_delay + + while True: + try: + return await func(*args, **kwargs) + except errors as e: + num_retries = _handle_retries(num_retries, max_retries, e) + delay = _compute_delay(delay, exponential_base, jitter) + logger.info( + "Error: %s. Retrying in %.2f seconds.", str(e), delay + ) + kwargs = _double_timeout(**kwargs) + await asyncio.sleep(delay) + + return wrapper + + return decorator + + +def _handle_retries(num_retries, max_retries, error): + """Raise error if retry attempts exceed max limit""" + num_retries += 1 + if num_retries > max_retries: + msg = f"Maximum number of retries ({max_retries}) exceeded" + raise ELMRuntimeError(msg) from error + return num_retries + + +def _compute_delay(delay, exponential_base, jitter): + """Compute the next delay time""" + return delay * exponential_base * (1 + jitter * random.random()) + + +def _double_timeout(**kwargs): + """Double timeout parameter if it exists in kwargs.""" + if "timeout" not in kwargs: + return kwargs + + prev_timeout = kwargs["timeout"] + logger.info( + "Detected 'timeout' key in kwargs. Doubling this input from " + "%.2f to %.2f for next iteration.", + prev_timeout, + prev_timeout * 2, + ) + kwargs["timeout"] = prev_timeout * 2 + return kwargs diff --git a/elm/version.py b/elm/version.py index a3ece25b..7ceff579 100644 --- a/elm/version.py +++ b/elm/version.py @@ -2,4 +2,4 @@ ELM version number """ -__version__ = "0.0.3" +__version__ = "0.0.4" diff --git a/elm/web/__init__.py b/elm/web/__init__.py new file mode 100644 index 00000000..fd6432a8 --- /dev/null +++ b/elm/web/__init__.py @@ -0,0 +1,3 @@ +"""ELM Web scraping. """ + +from .google_search import PlaywrightGoogleLinkSearch diff --git a/elm/web/document.py b/elm/web/document.py new file mode 100644 index 00000000..67ba17cd --- /dev/null +++ b/elm/web/document.py @@ -0,0 +1,244 @@ +# -*- coding: utf-8 -*- +"""ELM Web Document class definitions""" +from abc import ABC, abstractmethod +from copy import deepcopy +from functools import cached_property + +from elm.utilities.parse import ( + combine_pages, + clean_headers, + html_to_text, + remove_blank_pages, + format_html_tables, + replace_common_pdf_conversion_chars, + replace_multi_dot_lines, + remove_empty_lines_or_page_footers, +) + + +class BaseDocument(ABC): + """Base ELM web document representation.""" + + def __init__(self, pages, metadata=None): + """ + + Parameters + ---------- + pages : iterable + Iterable of strings, where each string is a page of a + document. + metadata : dict, optional + Optional dict containing metadata for the document. + By default, ``None``. + """ + self.pages = remove_blank_pages(pages) + self.metadata = metadata or {} + + @property + def empty(self): + """bool: ``True`` if the document contains no pages.""" + return not self.pages + + @cached_property + def raw_pages(self): + """list: List of (a limited count of) raw pages""" + if not self.pages: + return [] + + return self._raw_pages() + + @cached_property + def text(self): + """str: Cleaned text from document""" + if not self.pages: + return "" + + return self._cleaned_text() + + @abstractmethod + def _raw_pages(self): + """Get raw pages from document""" + raise NotImplementedError( + "This document does not implement a raw pages extraction function" + ) + + @abstractmethod + def _cleaned_text(self): + """Compute cleaned text from document""" + raise NotImplementedError( + "This document does not implement a pages cleaning function" + ) + + @property + @abstractmethod + def WRITE_KWARGS(self): + """dict: Dict of kwargs to pass to `open` when writing this doc.""" + raise NotImplementedError + + @property + @abstractmethod + def FILE_EXTENSION(self): + """str: Cleaned document file extension.""" + raise NotImplementedError + + +class PDFDocument(BaseDocument): + """ELM web PDF document""" + + CLEAN_HEADER_KWARGS = { + "char_thresh": 0.6, + "page_thresh": 0.8, + "split_on": "\n", + "iheaders": [0, 1, 3, -3, -2, -1], + } + """Default :func:`~elm.utilities.parse.clean_headers` arguments""" + WRITE_KWARGS = {"mode": "wb"} + FILE_EXTENSION = "pdf" + + def __init__( + self, + pages, + metadata=None, + percent_raw_pages_to_keep=25, + max_raw_pages=18, + num_end_pages_to_keep=2, + clean_header_kwargs=None, + ): + """ + + Parameters + ---------- + pages : iterable + Iterable of strings, where each string is a page of a + document. + metadata : str, optional + metadata : dict, optional + Optional dict containing metadata for the document. + By default, ``None``. + percent_raw_pages_to_keep : int, optional + Percent of "raw" pages to keep. Useful for extracting info + from headers/footers of a doc, which are normally stripped + to form the "clean" text. By default, ``25``. + max_raw_pages : int, optional + The max number of raw pages to keep. The number of raw pages + will never exceed the total of this value + + `num_end_pages_to_keep`. By default, ``18``. + num_end_pages_to_keep : int, optional + Number of additional pages to keep from the end of the + document. This can be useful to extract more meta info. + The number of raw pages will never exceed the total of this + value + `max_raw_pages`. By default, ``2``. + clean_header_kwargs : dict, optional + Optional dictionary of keyword-value pair arguments to pass + to the :func:`~elm.utilities.parse.clean_headers` + function. By default, ``None``. + """ + super().__init__(pages, metadata=metadata) + self.percent_raw_pages_to_keep = percent_raw_pages_to_keep + self.max_raw_pages = min(len(self.pages), max_raw_pages) + self.num_end_pages_to_keep = num_end_pages_to_keep + self.clean_header_kwargs = deepcopy(self.CLEAN_HEADER_KWARGS) + self.clean_header_kwargs.update(clean_header_kwargs or {}) + + @cached_property + def num_raw_pages_to_keep(self): + """int: Number of raw pages to keep from PDF document""" + num_to_keep = self.percent_raw_pages_to_keep / 100 * len(self.pages) + return min(self.max_raw_pages, max(1, int(num_to_keep))) + + @cached_property + def _last_page_index(self): + """int: last page index (determines how many end pages to include)""" + neg_num_extra_pages = self.num_raw_pages_to_keep - len(self.pages) + neg_num_last_pages = max( + -self.num_end_pages_to_keep, neg_num_extra_pages + ) + return min(0, neg_num_last_pages) + + def _cleaned_text(self): + """Compute cleaned text from document""" + pages = clean_headers(deepcopy(self.pages), **self.clean_header_kwargs) + text = combine_pages(pages) + text = replace_common_pdf_conversion_chars(text) + text = replace_multi_dot_lines(text) + text = remove_empty_lines_or_page_footers(text) + return text + + # pylint: disable=unnecessary-comprehension + # fmt: off + def _raw_pages(self): + """Get raw pages from document""" + raw_pages = [page for page in self.pages[:self.num_raw_pages_to_keep]] + if self._last_page_index: + raw_pages += [page for page in self.pages[self._last_page_index:]] + return raw_pages + + +class HTMLDocument(BaseDocument): + """ELM web HTML document""" + + HTML_TABLE_TO_MARKDOWN_KWARGS = { + "floatfmt": ".5f", + "index": True, + "tablefmt": "psql", + } + """Default :func:`~elm.utilities.parse.format_html_tables` arguments""" + WRITE_KWARGS = {"mode": "w", "encoding": "utf-8"} + FILE_EXTENSION = "txt" + + def __init__( + self, + pages, + metadata=None, + html_table_to_markdown_kwargs=None, + ignore_html_links=True, + text_splitter=None, + ): + """ + + Parameters + ---------- + pages : iterable + Iterable of strings, where each string is a page of a + document. + metadata : dict, optional + Optional dict containing metadata for the document. + By default, ``None``. + html_table_to_markdown_kwargs : dict, optional + Optional dictionary of keyword-value pair arguments to pass + to the :func:`~elm.utilities.parse.format_html_tables` + function. By default, ``None``. + ignore_html_links : bool, optional + Option to ignore link in HTML text during parsing. + By default, ``True``. + text_splitter : obj, optional + Instance of an object that implements a `split_text` method. + The method should take text as input (str) and return a list + of text chunks. The raw pages will be passed through this + splitter to create raw pages for this document. Langchain's + text splitters should work for this input. + By default, ``None``, which means the original pages input + becomes the raw pages attribute. + """ + super().__init__(pages, metadata=metadata) + self.html_table_to_markdown_kwargs = deepcopy( + self.HTML_TABLE_TO_MARKDOWN_KWARGS + ) + self.html_table_to_markdown_kwargs.update( + html_table_to_markdown_kwargs or {} + ) + self.ignore_html_links = ignore_html_links + self.text_splitter = text_splitter + + def _cleaned_text(self): + """Compute cleaned text from document""" + text = combine_pages(self.pages) + text = html_to_text(text, self.ignore_html_links) + text = format_html_tables(text, **self.html_table_to_markdown_kwargs) + return text + + def _raw_pages(self): + """Get raw pages from document""" + if self.text_splitter is None: + return self.pages + return self.text_splitter.split_text("\n\n".join(self.pages)) diff --git a/elm/web/file_loader.py b/elm/web/file_loader.py new file mode 100644 index 00000000..a71492c6 --- /dev/null +++ b/elm/web/file_loader.py @@ -0,0 +1,234 @@ +# -*- coding: utf-8 -*- +"""ELM Web file loader class.""" +import asyncio +import logging + +import aiohttp +from fake_useragent import UserAgent + +from elm.utilities.parse import read_pdf +from elm.web.document import PDFDocument, HTMLDocument +from elm.web.html_pw import load_html_with_pw +from elm.utilities.retry import async_retry_with_exponential_backoff +from elm.exceptions import ELMRuntimeError + + +logger = logging.getLogger(__name__) + + +async def _read_pdf_doc(pdf_bytes, **kwargs): + """Default read PDF function (runs in main thread)""" + pages = read_pdf(pdf_bytes) + return PDFDocument(pages, **kwargs) + + +async def _read_html_doc(text, **kwargs): + """Default read HTML function (runs in main thread)""" + return HTMLDocument([text], **kwargs) + + +class AsyncFileLoader: + """Async web file (PDF or HTML) loader""" + + DEFAULT_HEADER_TEMPLATE = { + "User-Agent": "", + "Accept": ( + "text/html,application/xhtml+xml,application/xml;" + "q=0.9,image/webp,*/*;q=0.8" + ), + "Accept-Language": "en-US,en;q=0.5", + "Referer": "https://www.google.com/", + "DNT": "1", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + """Default header""" + + def __init__( + self, + header_template=None, + verify_ssl=True, + aget_kwargs=None, + pw_launch_kwargs=None, + pdf_read_kwargs=None, + html_read_kwargs=None, + pdf_read_coroutine=None, + html_read_coroutine=None, + pdf_ocr_read_coroutine=None, + file_cache_coroutine=None, + browser_semaphore=None, + ): + """ + + Parameters + ---------- + header_template : dict, optional + Optional GET header template. If not specified, uses the + `DEFAULT_HEADER_TEMPLATE` defined for this class. + By default, ``None``. + verify_ssl : bool, optional + Option to use aiohttp's default SSL check. If ``False``, + SSL certificate validation is skipped. By default, ``True``. + aget_kwargs : dict, optional + Other kwargs to pass to :meth:`aiohttp.ClientSession.get`. + By default, ``None``. + pw_launch_kwargs : dict, optional + Keyword-value argument pairs to pass to + :meth:`async_playwright.chromium.launch` (only used when + reading HTML). By default, ``None``. + pdf_read_kwargs : dict, optional + Keyword-value argument pairs to pass to the + `pdf_read_coroutine`. By default, ``None``. + html_read_kwargs : dict, optional + Keyword-value argument pairs to pass to the + `html_read_coroutine`. By default, ``None``.. By default, ``None``. + pdf_read_coroutine : callable, optional + PDF file read coroutine. Must by an async function. Should + accept PDF bytes as the first argument and kwargs as the + rest. Must return a :obj:`elm.web.document.PDFDocument`. + If ``None``, a default function that runs in the main thread + is used. By default, ``None``. + html_read_coroutine : callable, optional + HTML file read coroutine. Must by an async function. Should + accept HTML text as the first argument and kwargs as the + rest. Must return a :obj:`elm.web.document.HTMLDocument`. + If ``None``, a default function that runs in the main thread + is used. By default, ``None``. + pdf_ocr_read_coroutine : callable, optional + PDF OCR file read coroutine. Must by an async function. + Should accept PDF bytes as the first argument and kwargs as + the rest. Must return a :obj:`elm.web.document.PDFDocument`. + If ``None``, PDF OCR parsing is not attempted, and any + scanned PDF URL's will return a blank document. + By default, ``None``. + file_cache_coroutine : callable, optional + File caching coroutine. Can be used to cache files + downloaded by this class. Must accept an + :obj:`~elm.web.document.Document` instance as the first + argument and the file content to be written as the second + argument. If this method is not provided, no document + caching is performed. By default, ``None``. + browser_semaphore : asyncio.Semaphore, optional + Semaphore instance that can be used to limit the number of + playwright browsers open concurrently. If ``None``, no + limits are applied. By default, ``None``. + """ + self.pw_launch_kwargs = pw_launch_kwargs or {} + self.pdf_read_kwargs = pdf_read_kwargs or {} + self.html_read_kwargs = html_read_kwargs or {} + self.get_kwargs = { + "headers": self._header_from_template(header_template), + "ssl": None if verify_ssl else False, + **(aget_kwargs or {}), + } + self.pdf_read_coroutine = pdf_read_coroutine or _read_pdf_doc + self.html_read_coroutine = html_read_coroutine or _read_html_doc + self.pdf_ocr_read_coroutine = pdf_ocr_read_coroutine + self.file_cache_coroutine = file_cache_coroutine + self.browser_semaphore = browser_semaphore + + def _header_from_template(self, header_template): + """Compile header from user or default template""" + headers = header_template or self.DEFAULT_HEADER_TEMPLATE + if not headers.get("User-Agent"): + headers["User-Agent"] = UserAgent().random + return dict(headers) + + async def fetch_all(self, *urls): + """Fetch documents for all requested URL's. + + Parameters + ---------- + *urls + Iterable of URL's (as strings) to fetch. + + Returns + ------- + list + List of documents, one per requested URL. + """ + outer_task_name = asyncio.current_task().get_name() + fetches = [ + asyncio.create_task(self.fetch(url), name=outer_task_name) + for url in urls + ] + return await asyncio.gather(*fetches) + + async def fetch(self, url): + """Fetch a document for the given URL. + + Parameters + ---------- + url : str + URL for the document to pull down. + + Returns + ------- + :class:`elm.web.document.Document` + Document instance containing text, if the fetch was + successful. + """ + doc, raw_content = await self._fetch_doc_with_url_in_metadata(url) + doc = await self._cache_doc(doc, raw_content) + return doc + + async def _fetch_doc_with_url_in_metadata(self, url): + """Fetch doc contents and add URL to metadata""" + doc, raw_content = await self._fetch_doc(url) + doc.metadata["source"] = url + return doc, raw_content + + async def _fetch_doc(self, url): + """Fetch a doc by trying pdf read, then HTML read, then PDF OCR""" + + async with aiohttp.ClientSession() as session: + try: + url_bytes = await self._fetch_content_with_retry(url, session) + except ELMRuntimeError: + return PDFDocument(pages=[]), None + + doc = await self.pdf_read_coroutine(url_bytes, **self.pdf_read_kwargs) + if doc.pages: + return doc, url_bytes + + text = await load_html_with_pw( + url, self.browser_semaphore, **self.pw_launch_kwargs + ) + doc = await self.html_read_coroutine(text, **self.html_read_kwargs) + if doc.pages: + return doc, doc.text + + if self.pdf_ocr_read_coroutine: + doc = await self.pdf_ocr_read_coroutine( + url_bytes, **self.pdf_read_kwargs + ) + + return doc, url_bytes + + @async_retry_with_exponential_backoff( + base_delay=2, + exponential_base=1.5, + jitter=False, + max_retries=3, + errors=( + aiohttp.ClientConnectionError, + aiohttp.client_exceptions.ClientConnectorCertificateError, + ), + ) + async def _fetch_content_with_retry(self, url, session): + """Fetch content from URL with several retry attempts""" + async with session.get(url, **self.get_kwargs) as response: + return await response.read() + + async def _cache_doc(self, doc, raw_content): + """Cache doc if user provided a coroutine""" + if doc.empty or not raw_content: + return doc + + if not self.file_cache_coroutine: + return doc + + cache_fn = await self.file_cache_coroutine(doc, raw_content) + if cache_fn is not None: + doc.metadata["cache_fn"] = cache_fn + return doc diff --git a/elm/web/google_search.py b/elm/web/google_search.py new file mode 100644 index 00000000..dbc0dac9 --- /dev/null +++ b/elm/web/google_search.py @@ -0,0 +1,138 @@ +# -*- coding: utf-8 -*- +"""ELM Web Scraping - Google search.""" +import asyncio +import logging + +from playwright.async_api import ( + async_playwright, + TimeoutError as PlaywrightTimeoutError, +) + +from elm.web.utilities import clean_search_query + + +logger = logging.getLogger(__name__) +_SEARCH_RESULT_TAG = '[jsname="UWckNb"]' + + +class PlaywrightGoogleLinkSearch: + """Search for top results on google and return their links""" + + EXPECTED_RESULTS_PER_PAGE = 10 + """Number of results displayed per Google page. """ + + def __init__(self, **launch_kwargs): + """ + + Parameters + ---------- + **launch_kwargs + Keyword arguments to be passed to + `playwright.chromium.launch`. For example, you can pass + ``headless=False, slow_mo=50`` for a visualization of the + search. + """ + self.launch_kwargs = launch_kwargs + self._browser = None + + async def _load_browser(self, pw_instance): + """Launch a chromium instance and load a page""" + self._browser = await pw_instance.chromium.launch(**self.launch_kwargs) + + async def _close_browser(self): + """Close browser instance and reset internal attributes""" + await self._browser.close() + self._browser = None + + async def _search(self, query, num_results=10): + """Search google for links related to a query.""" + logger.debug("Searching Google: %r", query) + num_results = min(num_results, self.EXPECTED_RESULTS_PER_PAGE) + + page = await self._browser.new_page() + await _navigate_to_google(page) + await _perform_google_search(page, query) + return await _extract_links(page, num_results) + + async def _skip_exc_search(self, query, num_results=10): + """Perform search while ignoring timeout errors""" + try: + return await self._search(query, num_results=num_results) + except PlaywrightTimeoutError as e: + logger.exception(e) + return [] + + async def _get_links(self, queries, num_results): + """Get links for multiple queries""" + outer_task_name = asyncio.current_task().get_name() + async with async_playwright() as pw_instance: + await self._load_browser(pw_instance) + searches = [ + asyncio.create_task( + self._skip_exc_search(query, num_results=num_results), + name=outer_task_name, + ) + for query in queries + ] + results = await asyncio.gather(*searches) + await self._close_browser() + return results + + async def results(self, *queries, num_results=10): + """Retrieve links for the first `num_results` of each query. + + This function executes a google search for each input query and + returns a list of links corresponding to the top `num_results`. + + Parameters + ---------- + num_results : int, optional + Number of top results to retrieve for each query. Note that + this value can never exceed the number of results per page + (typically 10). If you pass in a larger value, it will be + reduced to the number of results per page. + By default, ``10``. + + Returns + ------- + list + List equal to the length of the input queries, where each + entry is another list containing the top `num_results` + links. + """ + queries = map(clean_search_query, queries) + return await self._get_links(queries, num_results) + + +async def _navigate_to_google(page): + """Navigate to Google domain.""" + await page.goto("https://www.google.com") + await page.wait_for_load_state("networkidle") + + +async def _perform_google_search(page, search_query): + """Fill in search bar with user query and click search button""" + await page.get_by_label("Search", exact=True).fill(search_query) + await _close_autofill_suggestions(page) + await page.get_by_role("button", name="Google Search").click() + + +async def _close_autofill_suggestions(page): + """Google autofill suggestions often get in way of search button. + + We get around this by closing the suggestion dropdown before + looking for the search button. Looking for the "Google Search" + button doesn't work because it is sometimes obscured by the dropdown + menu. Clicking the "Google" logo can also fail when they add + seasonal links/images (e.g. holiday logos). Current solutions is to + look for a specific div at the top of the page. + """ + await page.locator("#gb").click() + + +async def _extract_links(page, num_results): + """Extract links for top `num_results` on page""" + links = await asyncio.to_thread(page.locator, _SEARCH_RESULT_TAG) + return [ + await links.nth(i).get_attribute("href") for i in range(num_results) + ] diff --git a/elm/web/html_pw.py b/elm/web/html_pw.py new file mode 100644 index 00000000..954381ce --- /dev/null +++ b/elm/web/html_pw.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""ELM Web HTML loading with Playwright + +We use Playwright so that javascript text is rendered before we scrape. +""" +import logging +from contextlib import AsyncExitStack + +from playwright.async_api import async_playwright +from playwright.async_api import Error as PlaywrightError +from playwright.async_api import TimeoutError as PlaywrightTimeoutError + +logger = logging.getLogger(__name__) + +# block pages by resource type. e.g. image, stylesheet +BLOCK_RESOURCE_TYPES = [ + "beacon", + "csp_report", + "font", + "image", + "imageset", + "media", + "object", + "texttrack", + # can block stylsheets and scripts, though it's not recommended: + # 'stylesheet', + # 'script', + # 'xhr', +] + + +# block popular 3rd party resources like tracking and advertisements. +BLOCK_RESOURCE_NAMES = [ + "adzerk", + "analytics", + "cdn.api.twitter", + "doubleclick", + "exelator", + "facebook", + "fontawesome", + "google", + "google-analytics", + "googletagmanager", + "lit.connatix", # <- not sure about this one +] + + +async def _intercept_route(route): # pragma: no cover + """intercept all requests and abort blocked ones + + Source: https://scrapfly.io/blog/how-to-block-resources-in-playwright/ + """ + if route.request.resource_type in BLOCK_RESOURCE_TYPES: + return await route.abort() + + if any(key in route.request.url for key in BLOCK_RESOURCE_NAMES): + return await route.abort() + + return await route.continue_() + + +async def load_html_with_pw( # pragma: no cover + url, browser_semaphore=None, **pw_launch_kwargs +): + """Extract HTML from URL using Playwright. + + Parameters + ---------- + url : str + URL to pull HTML for. + browser_semaphore : asyncio.Semaphore, optional + Semaphore instance that can be used to limit the number of + playwright browsers open concurrently. If ``None``, no limits + are applied. By default, ``None``. + **pw_launch_kwargs + Keyword-value argument pairs to pass to + :meth:`async_playwright.chromium.launch`. + + Returns + ------- + str + HTML from page. + """ + try: + text = await _load_html(url, browser_semaphore, **pw_launch_kwargs) + except (PlaywrightError, PlaywrightTimeoutError): + text = "" + return text + + +async def _load_html( # pragma: no cover + url, browser_sem=None, **pw_launch_kwargs +): + """Load html using playwright""" + if browser_sem is None: + browser_sem = AsyncExitStack() + + async with async_playwright() as p, browser_sem: + browser = await p.chromium.launch(**pw_launch_kwargs) + page = await browser.new_page() + await page.route("**/*", _intercept_route) + await page.goto(url) + await page.wait_for_load_state("networkidle", timeout=90_000) + text = await page.content() + + return text diff --git a/elm/web/utilities.py b/elm/web/utilities.py new file mode 100644 index 00000000..696be0c8 --- /dev/null +++ b/elm/web/utilities.py @@ -0,0 +1,116 @@ +# -*- coding: utf-8 -*- +"""ELM Web Scraping utilities.""" +import uuid +import hashlib +from pathlib import Path + +from slugify import slugify + + +def clean_search_query(query): + """Check if the first character is a digit and remove it if so. + + Some search tools (e.g., Google) will fail to return results if the + query has a leading digit: 1. "LangCh..." + + This function will take all the text after the first double quote + (") if a digit is detected at the beginning of the string. + + Parameters + ---------- + query : str + Input query that may or may not contain a leading digit. + + Returns + ------- + str + Cleaned query. + """ + query = query.strip() + if len(query) < 1: + return query + + if not query[0].isdigit(): + return query.strip() + + if (first_quote_pos := query[:-1].find('"')) == -1: + return query.strip() + + last_ind = -1 if query.endswith('"') else None + + # fmt: off + return query[first_quote_pos + 1:last_ind].strip() + + +def compute_fn_from_url(url, make_unique=False): + """Compute a unique file name from URL string. + + File name will always be 128 characters or less, unless the + `make_unique` argument is set to true. In that case, the max + length is 164 (a UUID is tagged onto the filename). + + Parameters + ---------- + url : str + Input URL to convert into filename. + make_unique : bool, optional + Option to add a UUID at the end of the file name to make it + unique. By default, ``False``. + + Returns + ------- + str + Valid filename representation of the URL. + """ + url = url.replace("https", "").replace("http", "").replace("www", "") + url = slugify(url) + url = url.replace("-", "").replace("_", "") + + url = _shorten_using_sha(url) + + if make_unique: + url = f"{url}{uuid.uuid4()}".replace("-", "") + + return url + + +def _shorten_using_sha(fn): + """Reduces FN to 128 characters""" + if len(fn) <= 128: + return fn + + out = hashlib.sha256(bytes(fn[64:], encoding="utf-8")).hexdigest() + return f"{fn[:64]}{out}" + + +def write_url_doc_to_file(doc, file_content, out_dir, make_name_unique=False): + """Write a file pulled from URL to disk. + + Parameters + ---------- + doc : elm.web.document.Document + Document containing meta information about the file. Must have a + "source" key in the `metadata` dict containing the URL, which + will be converted to a file name using + :func:`compute_fn_from_url`. + file_content : str | bytes + File content, typically string text for HTML files and bytes + for PDF file. + out_dir : path-like + Path to directory where file should be stored. + make_name_unique : bool, optional + Option to make file name unique by adding a UUID at the end of + the file name. By default, ``False``. + + Returns + ------- + Path + Path to output file. + """ + out_fn = compute_fn_from_url( + url=doc.metadata["source"], make_unique=make_name_unique + ) + out_fp = Path(out_dir) / f"{out_fn}.{doc.FILE_EXTENSION}" + with open(out_fp, **doc.WRITE_KWARGS) as fh: + fh.write(file_content) + return out_fp diff --git a/examples/ordinance_gpt/README.rst b/examples/ordinance_gpt/README.rst index 672116bb..230da971 100644 --- a/examples/ordinance_gpt/README.rst +++ b/examples/ordinance_gpt/README.rst @@ -5,11 +5,64 @@ Ordinance GPT This example folder contains supporting documents, results, and code for the Ordinance GPT experiment. -Code examples are being developed and will be uploaded before publication of -the Ordinance GPT paper. +Prerequisites +============= +We recommend installing the pytesseract module to allow PDF retrieval for scanned documents. +See the `ordinance-specific installation instructions `_ +for more details. + +Setup +===== +There are a few key things you need to set up in order to run ordinance retrieval and extraction. +First, you must specify which counties you want to process. You can do this by setting up a CSV file +with a ``County`` and a ``State`` column. Each row in the CSV file then represents a single county to process. +See the `example CSV `_ +file for reference. + +Once you have set up the county CSV, you can fill out the +`template JSON config `_. +See the documentation for the `"process_counties_with_openai" function `_ +for an explanation of all the allowed inputs to the configuration file. +Some notable inputs here are the ``azure*`` keys, which should be configured to match your Azure OpenAI API +deployment (unless it's defined in your environment with the ``AZURE_OPENAI_API_KEY``, ``AZURE_OPENAI_VERSION``, +and ``AZURE_OPENAI_ENDPOINT`` keys, in which case you can remove these keys completely), +and the ``pytesseract_exe_fp`` key, which should point to the pytesseract executable path on your +local machine (or removed from the config file if you are opting out of OCR). You may also have to adjust +the ``llm_service_rate_limit`` to match your deployment's API tokens-per-minute limit. Be sure to provide full +paths to all files/directories unless you are executing the program from your working folder. + +Execution +========= +Once you are happy with the configuration parameters, you can kick off the processing using + +.. code-block:: bash + + $ elm ords -c config.json + +You may also wish to add a ``-v`` option to print logs to the terminal (however, keep in mind that the code runs +asynchronously, so the the logs will not print in order). + +.. WARNING:: Running all of the 85 counties given in the sample county CSV file can cost $700-$1000 in API calls. We recommend running a smaller subset for example purposes. Source Ordinance Documents ========================== -The ordinance documents used in this example can be downloaded `here +The ordinance documents downloaded using (an older version of) this example code can be downloaded `here `_. + +Debugging +========= +Not sure why things aren't working? No error messages? Make sure you run the CLI call with a ``-v`` flag for "verbose" logging (e.g., ``$ elm ords -c config.json -v``) + +Errors on import statements? Trouble importing ``pdftotext`` with cryptic error messages like ``symbol not found in flat namespace``? Follow the `ordinance-specific install instructions `_ *exactly*. + +Extension to Other Technologies +=============================== +Extending this functionality to other technologies is possible but requires deeper understanding of the underlying processes. +We recommend you start out by examining the decision tree queries in `graphs.py `_ +as well as how they are applied in `parse.py `_. Once you +have a firm understanding of these two modules, look through the +`document validation routines ` to get a better sense of how to +adjust the web-scraping portion of the code to your technology. When you have set up the validation and parsing for your +technology, put it all together by adjusting the `"process_counties_with_openai" function `_ +to call your new routines. diff --git a/examples/ordinance_gpt/config.json b/examples/ordinance_gpt/config.json new file mode 100644 index 00000000..37767ec3 --- /dev/null +++ b/examples/ordinance_gpt/config.json @@ -0,0 +1,25 @@ +{ + "out_dir": ".", + "county_fp": "counties.csv", + "model": "gpt-4", + "azure_api_key": "", + "azure_version": "", + "azure_endpoint": "", + "llm_call_kwargs":{ + "temperature": 0, + "seed": 42, + "timeout": 300 + }, + "llm_service_rate_limit": 50000, + "td_kwargs": { + "dir": "." + }, + "tpe_kwargs": { + "max_workers": 10 + }, + "ppe_kwargs": { + "max_workers": 4 + }, + "pytesseract_exe_fp": "", + "log_level": "INFO" +} \ No newline at end of file diff --git a/examples/ordinance_gpt/counties.csv b/examples/ordinance_gpt/counties.csv new file mode 100644 index 00000000..838d066e --- /dev/null +++ b/examples/ordinance_gpt/counties.csv @@ -0,0 +1,86 @@ +County,State +San Luis Obispo,California +Jefferson,Colorado +Larimer,Colorado +Prowers,Colorado +Osceola,Florida +Burke,Georgia +Minidoka,Idaho +Ford,Illinois +Kankakee,Illinois +Menard,Illinois +Piatt,Illinois +Putnam,Illinois +Shelby,Illinois +Tazewell,Illinois +Franklin,Indiana +Morgan,Indiana +Ohio,Indiana +Steuben,Indiana +Whitley,Indiana +Black Hawk,Iowa +Cedar,Iowa +Cherokee,Iowa +Clarke,Iowa +Emmet,Iowa +Madison,Iowa +O'Brien,Iowa +Palo Alto,Iowa +Coffey,Kansas +Franklin,Kansas +Jackson,Kansas +Marion,Kansas +Pratt,Kansas +Seward,Kansas +Plymouth,Massachusetts +Branch,Michigan +Ogemaw,Michigan +Anoka,Minnesota +Chippewa,Minnesota +Cook,Minnesota +Faribault,Minnesota +Hennepin,Minnesota +Martin,Minnesota +Mcleod,Minnesota +Rice,Minnesota +Scott,Minnesota +Stearns,Minnesota +Wadena,Minnesota +Leake,Mississippi +Knox,Missouri +Cascade,Montana +Butler,Nebraska +Cedar,Nebraska +Loup,Nebraska +Valley,Nebraska +Humboldt,Nevada +San Miguel,New Mexico +Cattaraugus,New York +St. Lawrence,New York +Chowan,North Carolina +Jackson,North Carolina +Billings,North Dakota +Divide,North Dakota +Ramsey,North Dakota +Sargent,North Dakota +Fulton,Ohio +Miami,Ohio +Baker,Oregon +Clearfield,Pennsylvania +Lycoming,Pennsylvania +Mifflin,Pennsylvania +Northumberland,Pennsylvania +Beadle,South Dakota +Grant,South Dakota +Hand,South Dakota +Hyde,South Dakota +Marshall,South Dakota +Moody,South Dakota +Sanborn,South Dakota +Union,South Dakota +Archer,Texas +Denton,Texas +Guadalupe,Texas +Palo Pinto,Texas +Shenandoah,Virginia +Pierce,Wisconsin diff --git a/requirements.txt b/requirements.txt index 9ae6a510..aca4d724 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,10 +1,19 @@ openai>=1.1.0 aiohttp -tiktoken -scipy -pandas -numpy -PyPDF2 -networkx +click +fake_useragent +html2text +langchain +lxml matplotlib +networkx nrel-rex +nltk +numpy +pandas +playwright +PyPDF2 +python-slugify +scipy +tabulate +tiktoken diff --git a/setup.py b/setup.py index 37814d10..4bc1d60b 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ install_requires = f.readlines() -test_requires = ["pytest>=5.2", "pytest-mock"] +test_requires = ["pytest>=5.2", "pytest-mock", "pytest-asyncio", "pytest-cov"] description = "Energy Language Model" setup( @@ -36,15 +36,19 @@ license="BSD 3-Clause", zip_safe=False, keywords="elm", - python_requires='>=3.8', + python_requires='>=3.9', classifiers=[ "Development Status :: 4 - Beta", "Intended Audience :: Science/Research", "License :: OSI Approved :: BSD License", "Natural Language :: English", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", ], install_requires=install_requires, + extras_require={ + "dev": install_requires + test_requires, + }, + entry_points={"console_scripts": ["elm=elm.cli:main"]} ) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..b05bf503 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,109 @@ +# -*- coding: utf-8 -*- +"""Fixtures for use across all tests.""" +import asyncio + +import pytest +from openai.types import Completion, CompletionUsage, CompletionChoice +from openai.types.chat import ChatCompletionMessage + +from elm.ords.services.base import Service + + +LOGGING_META_FILES = {"exceptions.py"} + + +@pytest.fixture +def assert_message_was_logged(caplog): + """Assert that a particular (partial) message was logged.""" + caplog.clear() + + def assert_message(msg, log_level=None, clear_records=False): + """Assert that a message was logged.""" + assert caplog.records + + for record in caplog.records: + if msg in record.message: + break + else: + raise AssertionError(f"{msg!r} not found in log records") + + # record guaranteed to be defined b/c of "assert caplog.records" + # pylint: disable=undefined-loop-variable + if log_level: + assert record.levelname == log_level + assert record.filename not in LOGGING_META_FILES + assert record.funcName != "__init__" + assert "elm" in record.name + + if clear_records: + caplog.clear() + + return assert_message + + +@pytest.fixture +def service_base_class(): + """Base implementation of service for testing""" + job_order = [] + + class TestService(Service): + """Basic service implementation for testing.""" + + NUMBER = 0 + LEN_SLEEP = 0 + STAGGER = 0 + + def __init__(self): + """Initialize service.""" + self.running_jobs = set() + + @property + def can_process(self): + """True if number of running jobs less that the class number.""" + return len(self.running_jobs) < self.NUMBER + + async def process(self, job_id): + """Mock processing of input.""" + self.running_jobs.add(job_id) + job_order.append((self.NUMBER, job_id)) + await asyncio.sleep(self.LEN_SLEEP + self.STAGGER * job_id * 0.5) + self.running_jobs.remove(job_id) + return self.NUMBER + + return job_order, TestService + + +@pytest.fixture +def sample_openai_response(): + """Function to get sample openAI response that can be used for tests""" + + def _get_response( + content="test_response", + kwargs=None, + completion_tokens=10, + prompt_tokens=100, + total_tokens=110, + ): + usage = CompletionUsage( + completion_tokens=completion_tokens, + prompt_tokens=prompt_tokens, + total_tokens=total_tokens, + ) + choice = CompletionChoice( + finish_reason="stop", + index=0, + logprobs=None, + text="", + message=ChatCompletionMessage(content=content, role="assistant"), + ) + llm_response = Completion( + id="1", + choices=[choice], + created=0, + model=(kwargs or {}).get("model", "gpt-4"), + object="text_completion", + usage=usage, + ) + return llm_response + + return _get_response diff --git a/tests/data/Anoka Minnesota.txt b/tests/data/Anoka Minnesota.txt new file mode 100644 index 00000000..0afea149 --- /dev/null +++ b/tests/data/Anoka Minnesota.txt @@ -0,0 +1,4220 @@ + + + * + + * __ Notifications + + * __ Sign In + + * __ Help + * + +Code of Ordinances + +______ + +__ + + * __Recent Changes + * __Previous Versions + * __Notifications + * * __Sign In + +__ + + 1. Anoka, Minnesota - Code of Ordinances + 2. Chapter 78 - ZONING + 3. ARTICLE IX. - SUPPLEMENTAL REGULATIONS + 4. DIVISION 4. - WIND ENERGY CONVERSION SYSTEM + 5. Sec. 78-668. - Performance standards. + +__ Show Changes __ + +____ + +more __ + + * Code of Ordinances + * __Recent Changes + * __Pending Amendments + * __Previous Versions + * * MuniPRO + * __My Saved Searches + * __My Drafts + * __My Notes + * * __Show Walkthrough + +version: Aug 29, 2023 (current) __ + +____ __ + +## Anoka, MN +Code of Ordinances + +__ + + * __ ANOKA MINNESOTA CITY CODE + * SUPPLEMENT HISTORY TABLE modified + * __ Chapter 1 - GENERAL PROVISIONS + * __ Chapter 2 - ADMINISTRATION + * __ Chapter 6 - ALCOHOLIC BEVERAGES + * __ Chapter 10 - AMUSEMENTS AND ENTERTAINMENT + * __ Chapter 14 - ANIMALS + * __ Chapter 18 - BUILDINGS AND BUILDING REGULATIONS + * __ Chapter 22 - BUSINESSES AND SERVICES + * __ Chapter 26 - ELECTIONS + * __ Chapter 30 - ENVIRONMENT + * __ Chapter 34 - FIRE PREVENTION AND PROTECTION + * __ Chapter 38 - HERITAGE PRESERVATION + * __ Chapter 42 - LAW ENFORCEMENT + * __ Chapter 46 - OFFENSES + * __ Chapter 50 - PROPERTY MAINTENANCE + * __ Chapter 54 - STREETS, SIDEWALKS AND OTHER PUBLIC PLACES + * __ Chapter 58 - SUBDIVISIONS + * __ Chapter 62 - TAXATION + * __ Chapter 66 - TRAFFIC AND VEHICLES + * __ Chapter 70 - UTILITIES + * __ Chapter 74 - VEGETATION + * __ Chapter 78 - ZONING + * __ ARTICLE I. - IN GENERAL + * __ ARTICLE II. - ADMINISTRATION AND ENFORCEMENT + * __ ARTICLE III. - ZONING DISTRICTS ESTABLISHED, ZONING MAP + * __ ARTICLE IV. - CONDITIONAL USES + * __ ARTICLE V. - DISTRICT REGULATIONS + * __ ARTICLE VI. - MISSISSIPPI RIVER CONTROL CORRIDOR/RUM RIVER PROTECTION + * __ ARTICLE VII. - FLOODPLAINS + * __ ARTICLE VIII. - SIGNS + * __ ARTICLE IX. - SUPPLEMENTAL REGULATIONS + * __ DIVISION 1. - GENERALLY + * __ DIVISION 2. - OFF-STREET PARKING AND LOADING + * __ DIVISION 3. - TELECOMMUNICATION TOWERS + * __ DIVISION 4. - WIND ENERGY CONVERSION SYSTEM + * Sec. 78-664. - Purpose and intent. + * Sec. 78-665. - Definitions. + * Sec. 78-666. - Application; process; building permits; fees; inspections. + * Sec. 78-667. - Conditionally permitted and prohibited WECS. + * Sec. 78-668. - Performance standards. + * Sec. 78-669. - Other applicable standards. + * Secs. 78-670—78-689. - Reserved. + * __ DIVISION 5. - TRAFFIC ANALYSIS + * __ ARTICLE X. - NONCONFORMING USES AND DIMENSIONALLY SUBSTANDARD STRUCTURES + * CODE COMPARATIVE TABLE - PRIOR CODE + * CODE COMPARATIVE TABLE - LEGISLATION modified + * STATE LAW REFERENCE TABLE modified + +__Secs. 78-524—78-553. - Reserved. ARTICLE X. - NONCONFORMING USES AND +DIMENSIONALLY SUBSTANDARD STRUCTURES __ + + * ARTICLE IX. - SUPPLEMENTAL REGULATIONS + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + * DIVISION 1. - GENERALLY + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + + + * Sec. 78-554. - Prohibited dwelling units. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +No garage, recreational vehicle or trailer, camper, houseboat, automobile, +semi-trailer, tent, shed, storage container or other accessory building or +temporary structure may be used as a dwelling unit. + + * Sec. 78-555. - Accessory uses. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following accessory uses, in addition to those specified elsewhere in this +chapter, shall be permitted in any residential district, if the accessory uses +do not alter the character of the premises in respect to their use for the +purposes permitted in the district: + +(1) + +The operation of necessary facilities and equipment in connection with +schools, colleges, universities, hospitals and other institutions permitted in +the district. + +(2) + +Recreation, refreshment and service buildings in public parks and playgrounds. + +(Prior Code, § 74-481) + + * Sec. 78-556. - Accessory buildings. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Any accessory building in excess of 200 square feet must meet minimal +requirements of the state building code. + +(b) + +In case an accessory building is attached to the main building, it shall be +made structurally part of the main building and shall comply in all respects +with the requirements of this chapter applicable to the main building. + +(c) + +An accessory building, unless attached to and made a part of the main +building, shall not be closer than five feet to the main building, except as +otherwise provided in this section. + +(d) + +A detached accessory building shall not exceed 15 feet in height for a +building with a shed or flat roof, 18 feet in height for a gable, hip, +gambrel, mansard, arch or round roof, or the height of the principal building, +whichever is less. + +(e) + +The wall height of a detached accessory building shall not exceed 12 feet. + +(f) + +A detached accessory building shall not be located in any required front yard +or within five feet of any side or rear lot line. + +(g) + +In any residential zoning district the style, color, and facing material of a +garage shall be compatible with the principal building. No garage shall have a +facing material that consists of factory fabricated or pre-engineered steel or +finished metal panels or other similar material. + +(h) + +No accessory building in a business or mixed-use zoning district shall have a +facing material that consists of metal, aluminum or other similar materials. + +(i) + +In residential districts, temporary accessory buildings or containers used for +construction purposes are permitted for a period of up to six months after the +initial issuance of a building permit. Temporary buildings used for this +purpose may be of any material. + +(Prior Code, § 74-482) + + * Sec. 78-557. - Height regulations. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Where the average slope of a lot is greater than one foot rise or fall in +seven feet of horizontal distance from the established street elevation at the +property line, one story in addition to the number permitted in the district +in which the lot is situated shall be permitted on the downhill side of any +building. + +(b) + +In any district with a height limit of less than 50 feet, public and semi- +public buildings, schools, churches, hospitals and other institutions +permitted in the district may be erected to a height not exceeding 50 feet. +The front, rear and side yards shall be increased one foot for each one foot +by which the building exceeds the height limit established in this chapter for +such district. + +(c) + +Height limitations set forth elsewhere in this chapter may be increased by 100 +percent when applied to the following: + +(1) + +Monuments. + +(2) + +Flag poles. + +(3) + +Cooling towers. + +(4) + +Elevator penthouses. + +(d) + +Height limitations as set forth elsewhere in this chapter may be increased +with no limitation when applied to the following, provided that a conditional +use permit is issued to increase height: + +(1) + +Church domes, spires, belfries and roof ridges. + +(2) + +Schools, colleges and university buildings. + +(3) + +Chimneys or smokestacks. + +(4) + +Television and radio broadcasting antennas. + +(e) + +Height limitations set forth in the R-3, R-4, B-2 and B-3 districts may be +increased to six stories or 65 feet of height where the lot is not adjacent +to, or closer than 200 feet to any lot in any R-F, R-1 or R-2 district, and +provided a conditional use permit is issued for such height increase, as +required by this chapter. + +(Prior Code, § 74-483) + + * Sec. 78-558. - Area regulations. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +No lot shall be so reduced that the area of the lot or dimensions of the open +spaces shall be smaller than prescribed in this chapter. + +(Prior Code, § 74-484) + + * Sec. 78-559. - Yard regulations. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Measurements shall be taken from the nearest point of the wall of the building +to the lot line in question, subject to the following qualifications: + +(1) + +Cornices, canopies or eaves may extend into the required front yard a distance +not exceeding four feet, six inches. + +(2) + +Fire escapes may extend into the required front yard a distance not exceeding +four feet, six inches. + +(3) + +A landing place or uncovered porch may extend into the required front yard a +distance not exceeding six feet, if the landing place or porch has its floor +no higher than the entrance floor of the building. An open railing no higher +than three feet may be placed around such place. + +(4) + +The architectural features enumerated in subsections (1) through (3) of this +section may also extend into any side or rear yard to the same extent, except +that no porch, terrace or outside stairway shall project into the required +side yard distance, and except on existing lots that are 50 feet or less in +width, in such instance, allowable architectural features may project into the +required side yard a distance of two feet. + +(5) + +On double frontage lots, the required front yard shall be provided on both +streets. + +(6) + +In the districts where filling stations are allowed, pumps and pump islands +may be located within a required yard, provided that they are not less than 15 +feet from any street right-of-way lines. + +(7) + +The required minimum side yard for churches shall be 25 feet from any +residence lot line. + +(8) + +The required front yard of a corner lot shall not contain any wall, fence or +other structure, tree, shrub or other growth which may cause danger to traffic +on a street or public road by obscuring the view. + +(9) + +The required front yard of a corner lot shall be unobstructed above a height +of two feet and below a height of seven feet above the top of the curb line in +a triangular area, two sides of which are the lines running along the sides of +the streets or the curb lines from the point of intersection of the two street +lines as extended and a point 25 feet from such intersection and along each +street line the third side of the triangle being the line between the latter +two points. Also, boulevards between curb lines and right-of-way lines shall +be unobstructed above a height of two feet and below a height of seven feet +above the top of the curb line. + +(10) + +In determining the depth of rear yard for any building where the rear yard +opens into the alley, one-half the width of the alley, but not exceeding ten +feet, may be considered a portion of the rear yard, subject to the following +qualifications: + +a. + +The depth of any rear yard shall not be reduced to less than ten feet by the +application of this exception. + +b. + +If the door of any building or improvement, except a fence, opens toward an +alley, it shall not be erected or established closer than a distance of 15 +feet from the property line. + +(Prior Code, § 74-485) + + * Sec. 78-560. - Garages. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +No single-family or two-family dwelling shall be erected in any zoning +district unless a garage, detached or attached and covering an area of at +least 400 square feet, is also erected on the same parcel at the same time. A +certificate of occupancy shall not be issued by the building inspector until +all the work for which the building permit was issued has been completed. + +(Prior Code, § 74-486) + + * Sec. 78-561. - Trucks in residential districts. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +The following words, terms and phrases, when used in this section, shall have +the meanings ascribed to them in this subsection, except where the context +clearly indicates a different meaning. Definitions in M.S.A. § 168.002 are +adopted by reference as though set forth in this section. + +Height means a measurement taken from the ground to the highest point on the +vehicle at recommended tire pressure. All accessories, attachments, and +materials carried on the vehicle are considered part of the vehicle. + +Length means a measurement taken at the longest point of the vehicle or, if +the vehicle is a trailer, the horizontal distance between the front and rear +edges of the trailer bed. All accessories, attachments and materials carried +on the vehicle are considered part of the vehicle. + +Mid-size vehicle means any motorized vehicle or trailer more than eight feet +and up to nine feet in height, or more than 22 feet and up to 25 feet in +length, or more than 12,000 pounds and up to 15,000 pounds gross vehicles +weight. + +Oversize vehicle means any motorized vehicle or trailer more than nine feet in +height, or more than 25 feet in length, or more than 15,000 pounds gross +vehicle weight. + +(b) + +One mid-size vehicle or trailer may be parked or stored on a residential +property in accordance with off-street parking and loading regulations as +regulated by division 2 of this article. + +(c) + +One oversize recreational vehicle/recreational equipment that is owned by the +occupant of the premises may be parked or stored outside in a residence +district in accordance with off-street parking and loading regulations as +regulated by division 2 of this article. + +(d) + +Farm trucks, semi-trailers, special mobile equipment, truck tractors, farm +implements or tractors, trucks carrying or designed to carry explosive or +flammable materials, buses operated for hire or for commercial purposes, and +earth-moving equipment are prohibited from parking in residential zoning +districts, regardless of the length, height or gross vehicle weight. + +(e) + +This section shall not prohibit vehicles or trailers, as described in +subsections (b) through (d) of this section, from short-term parking of +vehicles when loading, unloading, or rendering a service. + +(f) + +No auxiliary motors or engines on any vehicle or equipment shall be allowed to +operate except when actively loading, unloading or performing a service. + +(g) + +The zoning administrator or his designee may grant an administrative waiver, +in writing, to a resident to allow a resident to temporarily park or store an +oversized vehicle outside at their place of residence once per year for a +period of up to seven days. + +(Prior Code, § 74-487) + + * Sec. 78-562. - Walls, fences, and hedges. + +modified + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +A fence is defined, for the purpose of this section, as any partition, +structure, wall, or gate erected as a divider marker, barrier or enclosure and +located along the boundary, or within the required yard. For the purpose of +this section, a fence shall not include naturally growing shrubs, trees or +other foliage. + +(b) + +No fence shall be erected or substantially altered in the city without +securing a permit from the building inspector. All such permits shall be +issued upon a written application which shall set forth the type of fence to +be constructed, the material to be used, the height and exact location of the +fence. A fee as determined by the city council shall be paid with each +application. + +(c) + +Fences, when constructed to enclose any lot or tract of land, shall be located +in such a way that the entire fence shall be on the property of the owner. +Posts and framework shall be placed within the property lines of the owner and +the actual fencing material, such as chainlink, lumber, pickets, etc., shall +be placed on the side of the fence which faces the street or adjacent +property. + +(d) + +No fence shall be allowed or constructed on-street rights-of-way. Fences may, +by permit, be placed on public utility easements so long as the structures do +not interfere in any way with existing underground or aboveground utilities. +The city or any utility company having authority to use such easements, shall +not be liable for repair or replacement of such fences in the event they are +moved, damaged or destroyed by virtue of the lawful use of such easement. + +(e) + +Fence heights in residential districts. + +(1) + +Fence height is measured from the fence owner's yard grade to the top of the +fence. + +(2) + +Fences four feet in height or less may be placed anywhere on a lot, unless +otherwise restricted. + +(3) + +Fences above four feet in height up to a maximum of 6 feet in height may be +placed anywhere on a lot but not in a front yard. + +a. + +On riparian lots, the front yard is defined as the yard which abuts the water. + +b. + +Riparian lots also abutting a public right-of-way shall be considered to have +two front yards. + +c. + +Corner lots and through lots shall be considered to have two front yards. + +d. + +Lots that have no defined front yard shall be designated a single front yard +as determined by the zoning administrator. + +(f) + +The required front yard of a corner lot shall not contain any fence which may +cause danger to traffic on a street or public road, by obscuring a driver's +view. On corner lots, no fence shall be permitted within the intersection +sight distance triangle. + +(g) + +Off-street parking and loading zones and landscaped areas for nonresidential +and for multifamily residential development adjoining one- or two-family +residence districts shall be screened by a minimum of a six-foot-high fence or +a planting buffer screen. Plans of such screen or fence shall be submitted for +approval as part of the site plan review by the planning commission and the +city council. Such plans shall be part of the application for a building +permit and such fence or landscaping shall be installed as part of the initial +construction and be maintained in a sightly condition, compatible with the +surrounding area. + +(h) + +Every fence shall be constructed in a workmanlike manner and of substantial +material reasonably suited to the purpose for which the fence is to be used. +Barbed wire is not allowed in any residence district but may be installed in +commercial or industrial districts with approval by the building inspector. + +(1) + +Fence materials. The following fence materials are allowed in all residential +districts unless otherwise stated in this chapter: + +a. + +Treated wood, cedar or redwood; + +b. + +Composite, including plastic or simulated wood; + +c. + +Decorative rick or stone; + +d. + +Wrought iron or aluminum designed to simulate wrought iron; + +e. + +Coated or noncoated chainlink; + +f. + +Split rail; + +g. + +Other materials or fence types approved by the city. + +(2) + +Maintenance. Every fence shall be maintained in a condition of good repair and +shall not be allowed to become and remain in a condition that would constitute +a public nuisance or a dangerous condition. The building inspector is +authorized to notify the owner to the condition and allow the owner 60 days in +which to repair or demolish the fence. + +(3) + +Construction standards. Fences shall be constructed in conformity with the +wind, stress, foundation, structural and other requirements of the state +building code when applicable. + +(Prior Code, § 74-488; Ord. No. 2021-1736 , § 1, 2-1-2021; Ord. No. +2022-1755 , § 1, 1-3-2022) + + * Sec. 78-563. - Tree preservation. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following are standards of preservation during construction or grading: + +(1) + +Intent. Developments, structures, utilities, and all other site activities +must be designed, installed, and constructed so that the maximum numbers of +trees are preserved on all lots or parcels. + +(2) + +Definition. For the purpose of this section, a significant tree shall be +defined as any live, healthy tree measuring eight inches in diameter or +greater, measured at 4½ feet above the ground. + +(3) + +Tree preservation plan required. To minimize tree loss and to mitigate tree +removal on wooded lots or parcels with trees, a tree preservation plan must be +submitted for approval along with any land disturbance permit, grading permit, +site plan, or plat approval. All site activity associated with the proposed +permit or plat must be in compliance with the approved tree preservation plan. + +(4) + +Tree preservation plan. A registered architect, landscape architect, forester, +or engineer must prepare the tree preservation plan. The plan must include a +scaled drawing or survey, including the following information: + +a. + +A tree inventory indicating the amount, species, location and condition of all +existing significant trees and clumps of nonsignificant trees within the +limits of the proposed activity. + +b. + +Identification of significant trees to be protected, preserved, undisturbed or +to be removed. + +c. + +Location of existing and proposed structures, improvements, utilities and +existing and proposed contours. + +d. + +Protection techniques that will be utilized to minimize disturbance to all +trees remaining on-site. Trees must be protected from direct and indirect root +damage and trunk and crown disturbance. The following preservation standards +apply: + +1\. + +Construction activities, including parking, material storage, dirt +stockpiling, concrete washout and other similar activities, must be done as to +not damage or destroy a significant tree. + +2\. + +Protective fencing must be installed around trees that are not being removed. +Such fences must be at least four feet high and must consist of polyethylene +safety fencing. Fencing must remain in place until construction is completed +or other landscaping has been installed and the city forester has approved the +removal of the fencing. + +e. + +A tree replacement plan indicating size, species, location, and planting +specifications of all street and replacement trees. + +(5) + +Tree replacement. + +a. + +Each significant tree removed or damaged through construction or grading, or +found to have been damaged within one year after completion of construction, +must be replaced on-site at a ratio of 1:1 except for: + +1\. + +Nonresidential zoned property. In no case need the tree replacement density +exceed eight trees per acre in nonresidential zoned districts. + +2\. + +Residential zoned property. In no case need the tree replacement density +exceed eight trees per acre on lots one acre or more or subdivisions that +occur on unplatted land over one acre. On residentially zoned lots less than +one acre, a 1:1 replacement of all trees will be required for the first seven +trees removed from the lot. + +3\. + +Trees not to be replaced. Significant trees removed that the city forester +determines to be undesirable, invasive, or diseased shall not need to be +replaced. + +b. + +Street trees shall not be counted towards the number of replacement trees +required on a site. + +c. + +Replacement trees shall be a minimum 2½ inches in diameter if deciduous, or +six feet in height if coniferous, measured at 4½ feet above ground, and shall +be a species similar to those which were destroyed, unless otherwise required +by the city forester. Replacement trees shall be balled and burlap. + +d. + +Mississippi River Control Corridor/Rum River Corridor. Any lands within the +Mississippi River Control Corridor/Rum River Corridor shall meet tree +replacement/preservation regulations set forth in section 78-403. + +(6) + +Tree replacement fee. If the developer is unable to replace the required +amount of trees due to physical circumstances unique to the site, a tree +replacement fee in an amount established by the city council shall be paid in +lieu of tree replacement. + +(7) + +Trees on public property. Trees on public property shall be regulated by +chapter 74, article IV. + +(8) + +Inspection and enforcement. Prior to commencement of site grading or +excavation, the site shall be staked and fenced for tree protection per the +approved tree preservation plan. Construction activities shall cease until +compliance with the tree preservation plan has been achieved. Violations of +this section shall be considered a misdemeanor. + +(Prior Code, § 74-489) + + * Sec. 78-564. - Metal roof. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Prefinished metal roofs are permitted in all districts, provided: + +(1) + +The metal roof shall not have exposed fasteners, semi-concealed fasteners, or +any fastener system that does not adhere directly to the support system. + +(2) + +Any metal roof that is not a high-quality commercial thickness/weight +according to the building code is prohibited. + +(3) + +Any metal roof that has not been treated with a factory applied color-coating +system is prohibited. + +(4) + +The metal roof must have a color retention guarantee minimum of 20 years. + +(5) + +There shall be no open ended rivets or seams where the roofline meets the +fascia. + +(b) + +Single-family homes, townhomes and row homes shall be allowed to use slate, +shingle, shake, tile, or similar design pre-finished metal roofs. Standing +seam metal roof design is not allowed on single-family homes, townhomes, and +row homes, with the exception of copper accents or trim. + +(Prior Code, § 74-490) + + * Sec. 78-565. - Temporary accessory buildings. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Definitions. The following words, terms and phrases, when used in this +section, shall have the meanings ascribed to them in this subsection, except +where the context clearly indicates a different meaning: + +Temporary accessory building means a building used for a temporary purpose +which has a roof but is without a foundation or footings, is designed to be +removable, and is not designed to be permanently attached to the ground, to +another structure, or to any utility system. Such buildings are typically +constructed of a canvas or other fabric over a PVC, metal or wood frame. + +(b) + +One temporary accessory building is permitted on each parcel in all +residential districts, subject to the following standards: + +(1) + +A temporary accessory building permit must be obtained. + +(2) + +The area of the temporary accessory building will be included in the +impervious surface calculations for the property. + +(3) + +The size of the temporary accessory building shall not exceed 12 feet by 26 +feet. + +(4) + +The temporary accessory building shall be securely anchored to withstand the +weather and prevent against collapsing. + +(5) + +The temporary accessory building shall be placed in the rear yard, a minimum +of five feet from either the side or rear lot line. For riparian lots, the +temporary building must be placed on the river side of the property and must +meet the structure setback requirements from the river or placed no closer +than that of the existing primary structure if the primary structure does not +meet setback requirements. In the case of a corner lot, a temporary accessory +building may be located in a side yard. + +(6) + +The temporary accessory building can be placed on the site for a period of no +more than six months per calendar year. In cases where weather prevents timely +removal, one 30-day extension may be granted administratively. Such extension +shall require an extension permit. + +(7) + +The temporary accessory building must be constructed of durable, fire +retardant materials. + +(8) + +The temporary accessory building shall not exceed the height of any other +accessory structures on the site or 15 feet, whichever is less. + +(9) + +For the purposes of this section, tents and canopies erected for events, +weddings, family gatherings, etc., are not required to get a temporary +building permit if erected for a period of two weeks or less. + +(10) + +All applicable requirements of the state building code and the state fire code +shall be met. + +(11) + +Materials stored in the temporary accessory building must meet the standards +of the state fire code. + +(12) + +The temporary accessory building must remain in good repair throughout the +time it is erected on the site. Frames without a covering are not permitted. + +(13) + +A temporary accessory building erected on a site shall be counted toward the +maximum number of accessory buildings allowed by this section. + +(Prior Code, § 74-491) + + * Sec. 78-566. - Accessory structure administrative site plan approval. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +For the purpose of enforcing this chapter, an accessory structure site plan +approval shall be required of all persons intending to erect, alter, or place +any building or structure that is otherwise exempt from needing a building +permit under Minn. R. 1300.0120(4)(A)(1). + +(b) + +The accessory structure site plan review shall be approved by the zoning +administrator or his designee upon a written finding that the proposal meets +the requirements of the applicable zoning district and is in compliance with +the relevant chapter standards. + +(c) + +Administrative site plan approval shall be processed according to the +procedures and criteria set forth in section 78-36(g). + +(d) + +Application materials. The person seeking site plan approval must complete an +application and submit the completed application to the zoning administrator. +The review fee shall be established by the city council and recorded in the +city fee schedule. The applicant shall submit the following information as +part of the application: + +(1) + +A site plan showing the following information: + +a. + +Location and dimensions of lot lines, buildings, driveways, off-street parking +spaces, sidewalks, patios, and other forms of impervious lot coverage as +determined by the zoning administrator. + +b. + +Distances between buildings. + +c. + +Front, side, and rear lot lines with dimensions. + +d. + +Location of any easements or underground utilities. + +e. + +Other information deemed necessary to determine compliance with this chapter. + +(2) + +A narrative describing how the structure will be used. + +(3) + +A signed statement by the applicant stating that they are aware that this +chapter prohibits residential occupancy and home occupations in accessory +structures. + +(4) + +Any other information requested by the zoning administrator in order to allow +a reasonable review of the requested proposal. + +(Prior Code, § 74-492) + + * Secs. 78-567—78-595. - Reserved. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + * DIVISION 2. - OFF-STREET PARKING AND LOADING + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + + + * Sec. 78-596. - Application of parking and loading regulations. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The regulations and requirements set forth in this division shall apply to all +off-street parking facilities, including driveways, parking lots and storage +areas, in all zoning districts of the city unless otherwise exempted in this +chapter. + +(Prior Code, § 74-506) + + * Sec. 78-597. - Site plan drawing necessary. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +All applications for a building permit, driveway permit, or certificate of +occupancy in all zoning districts shall be accompanied by a site plan drawn to +scale indicating the location and dimensions of the driveway, off-street +parking and loading spaces, and storage areas, and a description of materials +to be used in compliance with the requirements set forth in this chapter. All +applications shall be submitted to the planning department. The planning +department will distribute the application to the appropriate departments for +review and will issue the permit. + +(Prior Code, § 74-507) + + * Sec. 78-598. - Minimum area regulations. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Each parking space shall be the following size or larger based on the angle of +parking: + +_Expand_ + ++----+------------------+-------------+---------------------+------------------------+------------------+ +| | 0 | 1 | 2 | 3 | 4 | +|----+------------------+-------------+---------------------+------------------------+------------------| +| 0 | Angle of Parking | Stall Width | Stall Depth to Curb | Traffic Flow Direction | Drive Lane Width | +| 1 | 45° | 9' | 22' | One-way | 14' | +| 2 | 60° | 9' | 21' | One | 16' | +| 3 | 75° | 9' | 21' | One | 18' | +| 4 | 90° | 9' | 18' | Two-way | 24' | ++----+------------------+-------------+---------------------+------------------------+------------------+ + + + +(b) + +Exceptions may be made for compact vehicle spaces under the following +conditions: + +(1) + +The design promotes compact car stall use (e.g., designing all compact stalls +at the entrance of the lot). + +(2) + +All compact car stalls are clearly designated by signage. + +(3) + +No more than 40 percent of all required parking stalls are designated for +compact cars. Each compact parking space shall be no less than eight feet by +18 feet. + +(Prior Code, § 74-508) + + * Sec. 78-599. - Computing requirements. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +In computing the number of parking spaces required, the following rules shall +govern: + +(1) + +Floor space means the gross floor area of the specific use. + +(2) + +When determining the number of off-street parking spaces, fractional results +of one-half or more shall constitute another space. + +(3) + +The parking space requirement for a use not specifically mentioned in this +division shall be the same as required for a use of similar nature as +determined by the city planning commission. + +(4) + +In stadiums, sports arenas, churches, and other places of public assembly in +which patrons or spectators occupy benches, pews, or other similar seating +facilities, each 22 inches of such seating facilities shall be counted as one +seat for the purpose of determining requirements. + +(Prior Code, § 74-509) + + * Sec. 78-600. - Reduction and use of parking and loading space. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +When demonstrated to the satisfaction of the city council that up to ten +percent of the number of parking spaces required by this division would not be +needed for the particular use in question, a reduced number of parking spaces +may be approved subject to the following: + +(1) + +The application for reduction shall be accompanied by supporting data +specifically applying to the particular use in question. + +(2) + +The applicant must also provide each of the following: + +a. + +A detailed parking plan demonstrating that the parking otherwise required by +this division can be provided on the site within chapter design standards; and + +b. + +A covenant in recordable form, approved as to form and content by the city +attorney, executed by all property owners, which covenant provides that the +owners, heirs, successors and assigns, will not use the area identified for +expansion parking for any use except landscaping or to cause compliance with +the off-street parking requirements of this division. + +(3) + +The city may order installation of previously exempted parking spaces at any +time when, in the city's judgment, conditions indicate the need for such +parking, and the property owner shall comply with such order. + +(Prior Code, § 74-510) + + * Sec. 78-601. - Fences and planting screens. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Off-street parking and loading areas located in commercial, industrial, and +multifamily districts and adjoining residence districts shall be screened by a +minimum six-foot-high fence, wall or a planted buffer screen; plans of such +screen, fence, or wall shall be submitted for approval as part of the +application for a building permit, and such fence, wall or landscaping shall +be installed as a part of the initial construction. + +(Prior Code, § 74-511) + + * Sec. 78-602. - Access. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Parking and loading space shall have proper access from a public right-of-way. +The number and width of access drives shall be so located as to minimize +traffic congestion and traffic hazard. + +(Prior Code, § 74-512) + + * Sec. 78-603. - Location of parking facilities. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +All off-street parking facilities required by this division shall be located +and restricted as follows: + +(1) + +Required off-street parking shall be on the same lot under the same ownership +as the principal use being served, or within 200 feet pedestrian travel +distance thereof. + +(2) + +Head-in parking, directly off of and adjacent to a public street, with each +stall having its own direct access to the public street, shall be prohibited, +except for single-family, two-family, townhouses, and quadhome dwellings and +public safety buildings. + +(3) + +The boulevard portion of the street right-of-way shall not be used for +parking. + +(Prior Code, § 74-513) + + * Sec. 78-604. - Parking lots in residential districts. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +When in its opinion the best interests of the community will be served +thereby, the city council may permit, temporarily or permanently, the use of +land in a residential district, other than the single- and two-family +residential districts, for a parking lot provided that: + +(1) + +A conditional use permit is issued under article IV, division 2 of this +chapter. + +(2) + +The lot is not to be used for sales, storage, repair work or servicing of any +kind. + +(3) + +Entrance to and exit from the lot are to be located on the lot. + +(4) + +No advertising sign or material is to be located on the lot. + +(5) + +All parking is to be kept back of the setback building line by barrier unless +otherwise specifically authorized by the city council. + +(6) + +All lighting is to be arranged so that there will be no glare there from +annoying to the occupants of adjoining property in a residential district. + +(7) + +Surfacing of the parking lot is to be smoothly graded, hard surfaced and +adequately drained. + +(8) + +Any other conditions, such as screening, as may be deemed necessary by the +city council to protect the character of the residential district. + +(9) + +A parking lot may not be constructed for use by single- or two-family +dwellings. + +(10) + +The city council shall review parking lots in residential districts annually +to determine suitability for continued use. + +(Prior Code, § 74-514) + + * Sec. 78-605. - Yards. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Off-street parking and loading facilities shall be subject to the front yard, +side yard, and rear yard regulations for the use district in which the parking +is located; except that in the classes of B-1 and B-2 business districts and +industrial districts, no off-street parking or loading shall be located within +ten feet of any property line that abuts a street right-of-way or any of the +classes of residence districts; and except that in the classes of R-3 and R-4 +residence districts, no parking or loading shall be located within five feet +of any property line. + +(Prior Code, § 74-515) + + * Sec. 78-606. - Combined facilities. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Off-street parking facilities for a combination of mixed buildings, structures +or uses may be provided collectively in any business or industrial district in +which separate parking facilities for each separate building, structure or use +would be required, provided that the total number of spaces provided shall +equal the projected peak hour parking demand of the combined uses, subject to +the following special conditions: + +(1) + +A conditional use permit is issued under article IV, division 2 of this +chapter. + +(2) + +The owner of the property affected, along with the operators of all businesses +to utilize the combined parking facilities, shall join in the permit +application. + +(3) + +The proposed parking plan shall realistically project peak use of the combined +facilities based upon the proposed uses, and shall provide adequate spaces for +that peak demand. + +(4) + +All off-street parking facilities shall be located within 200 feet of the +building or use for which the permit is issued. + +(5) + +A properly drawn legal instrument, executed by the parties concerned for joint +use of off-street parking facilities duly approved as to form and manner of +execution by the city attorney, shall be filed with the city clerk. + +(b) + +A conditional use permit for combined parking facilities shall restrict the +uses of the affected property to those designated in the permit until and +unless the permit is amended or rescinded. Such a permit may be revoked if +parking demand for the combined uses exceeds the capacity of the combined +facilities; however, the use restrictions of the permit shall remain in effect +after such a revocation until and unless they are specifically removed by +council resolution to that effect. + +(Prior Code, § 74-516) + + * Sec. 78-607. - Construction and maintenance standards applicable to all driveways. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Definitions. The following words, terms and phrases, when used in this +section, shall have the meanings ascribed to them in this subsection, except +where the context clearly indicates a different meaning: + +Driveway means the portion of a lot that is designed to provide vehicular +access between a road or alley and a parking or loading space, including the +driveway apron. + +(b) + +A driveway permit shall be required for replacing, constructing, improving, +expanding, resurfacing, or altering driveways, driveway aprons, and parking +areas, unless otherwise approved through a site plan process or other city +approval. An application for a driveway permit shall be submitted to the +city's planning department. The planning department will route the permit to +the appropriate departments for review and will issue the driveway permit. + +(c) + +In all zoning districts, parking areas and driveways shall be paved with +asphalt or concrete and designed to prevent damage to adjacent properties by +surface water runoff and to minimize the amount of paved areas on the site. +The following driveways are exempt from this requirement upon approval of the +city engineer: + +(1) + +Driveways serving a recreational area. + +(2) + +Driveways constructed of alternative materials that function similarly to +those listed in this subsection (c). + +(3) + +Storage areas for heavy construction equipment that would damage the pavement. +Such storage areas shall have an approved maintenance and drainage plan. + +(d) + +The city shall have the right to review and inspect all driveway construction. + +(e) + +Driveways shall have a maximum slope of eight percent unless otherwise +approved by the city engineer. + +(f) + +Porous pavers or porous paving systems may be used upon approval of the city +engineer. + +(g) + +Driveways and parking areas shall comply with the impervious surface and lot +coverage restrictions of the zoning district within which it will be +constructed. + +(Prior Code, § 74-517) + + * Sec. 78-608. - Striping. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Except for single-family, two-family, townhouses, and quadhome dwellings, all +paved parking stalls shall be marked with white or yellow painted lines not +less than four inches wide and shall be properly maintained. + +(Prior Code, § 74-518) + + * Sec. 78-609. - Lighting. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +All off-street parking areas for residential uses of 12 or more spaces and all +off-street parking for commercial, industrial, institutional, and public uses +shall be equipped with operable lighting designed to illuminate the entire +surface of the parking area. + +(b) + +Any lighting used to illuminate the off-street parking area shall be arranged +as to reflect the light away from any adjacent properties, streets, or +highways. + +(Prior Code, § 74-519) + + * Sec. 78-610. - Required number of off-street parking spaces. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Off-street parking areas of sufficient size to provide parking for patrons, +customers, suppliers, visitors and employees shall be provided on the premises +of each use. The minimum number of required off-street parking spaces for the +following uses shall be as follows in all zoning districts in the city, except +the B-3 General Business District: + +(1) + +Automobile service stations: Four parking spaces plus two parking spaces for +each service stall; such parking spaces shall be in addition to parking space +required for gas pump areas. + +(2) + +Automobile sales, trailer sales, marine and boat sales, implement sales, +garden supply stores, building materials sale, and auto repair: Six parking +spaces plus one parking space for each 500 square feet of floor area over +1,000 square feet. + +(3) + +Assembly or exhibition halls, auditoriums, theaters, or sports arenas: One +parking space for each four seats, based upon design capacity. + +(4) + +Banks: At least one parking space for each 400 square feet of floor area. + +(5) + +Bed and breakfasts: One space per guest room and two for management. + +(6) + +Boardinghouses and lodginghouses: At least three parking spaces plus one +parking space for each three persons for whom living accommodations are +provided. + +(7) + +Bowling alleys: At least seven parking spaces for each alley, plus such +additional spaces as may be required for affiliated uses. + +(8) + +Car washes: In addition to required stacking spaces: + +a. + +Automatic drive-through service: Ten spaces or one space for each employee on +the maximum shift, whichever is greater. + +b. + +Self-service: A minimum of two spaces. + +c. + +Service station with car wash: No additional to that required for the station. + +(9) + +Churches: One parking space for each four seats, based on the design capacity +of the main seating area. + +(10) + +Convalescent or nursing homes: One parking space for each four beds for which +accommodations are offered. + +(11) + +Drive-in establishments and convenience food: One parking space for each 150 +square feet of gross floor area, but not less than 15 spaces. + +(12) + +Furniture and appliance stores, stores for repair of household equipment or +furniture: At least one parking space for each 600 square feet of floor area. + +(13) + +Golf courses, golf clubhouses, country clubs, swimming clubs, tennis clubs, +public swimming pools: 20 spaces plus one space for each 300 square feet of +floor area in the principal structure. + +(14) + +Hospitals: One parking space for each three hospital beds, plus one parking +space for each employee on the major shift. + +(15) + +Miniature golf courses, archery ranges or golf courses, driving ranges: Ten +parking spaces. + +(16) + +Motels, hotels: One space per each rental unit plus one space for each ten +units and one additional space for each employee on any shift. + +(17) + +Municipal administration buildings, community centers, public libraries, +museums, art galleries, post offices, and other municipal service buildings: +Ten parking spaces plus one parking space for each 500 square feet of floor +area in the principal structure. + +(18) + +Private clubs and lodges: One parking space for each 2½ seats. + +(19) + +Professional offices, medical and dental clinics and animal hospitals: One +space for each 200 square feet of floor area, but not less than three spaces +per lot design. + +(20) + +Residential uses: + +a. + +Single-family dwellings: Enclosed garage of at least 440 square feet. + +b. + +Two-family dwellings and quadhomes: A minimum of two spaces per dwelling unit +and an enclosed garage of at least 400 square feet. + +c. + +Townhouses: A minimum of two spaces per unit. At least one space per unit +shall consist of an enclosed garage. + +d. + +Multiple-dwellings: A minimum of 2½ spaces per unit. At least one space per +unit shall consist of an enclosed garage. + +(21) + +Restaurants, cafes, private clubs serving food or drinks, bars, or nightclubs: +One space for each 40 square feet of gross floor area of dining and bar area +and one additional space for each 80 square feet of kitchen area. + +(22) + +Shopping centers: In a B-2 Shopping Center Business District where several +business uses are grouped together according to a general development plan, +off-street automobile parking shall be provided in a ratio of not less than +four spaces per 1,000 square feet of gross leasable area, and separate off- +street space shall be provided for loading and unloading. + +(23) + +Sporting and health clubs: One space per 100 square feet of building area, +plus six spaces per tennis/racquetball or other type of court. + +(24) + +Supermarkets, discount houses, mail order outlets, retail stores, and other +stores with high customer volume: At least one parking space for each 250 +square feet of floor area. + +(25) + +Other uses: Other uses not specifically mentioned in this section shall be +determined on an individual basis by the city council. Factors to be +considered in such determination shall include, without limitation, size of +building, type of use, number of employees, expected volume and turnover of +customer traffic, and expected frequency and number of delivery or service +vehicles. + +(Prior Code, § 74-522) + + * Sec. 78-611. - Parking regulations for single-family and two-family residences. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +No owner or tenant of a single- or two-family residential property shall allow +any motor vehicle or trailer to be parked on such property except on a +driveway, within a garage, or on the side or rear yard area of the property as +specifically permitted in subsection (c) of this section. Every motor vehicle +or trailer that is parked outside of a garage shall display license plates +with current registration tags. No vehicle or trailer shall be permitted to +park in the sight triangle which is required to be unobstructed by section +78-559(9). With regard to outdoor parking, storage or repair of trucks and +equipment, see section 78-561. + +(b) + +No more than four motor vehicles, trailers, or combination thereof shall be +permitted to park on the driveways of any single-family residential property +on more than two days within any one-week period, except when a waiver is +obtained as provided in this section. Upon application to the zoning +administrator, waiver of this restriction may be obtained for a reasonable, +necessary, and discreet time period, not exceeding two weeks for social guest +parking, and not exceeding 90 days for the demolition of an existing garage +and construction of a new one. + +(c) + +Two motor vehicles or trailers per dwelling unit may be parked on the side or +rear yard of the property, off the driveway, at least five feet from the +property line, provided that the area around and under the motor vehicle or +trailer is maintained in a neat and orderly manner, including keeping weeds +and grass in the area mowed to a height of six inches or less. + +(d) + +For the purpose of this section, the term "motor vehicle" includes any self- +propelled vehicle which is required to be registered with the state department +of motor vehicles and to display a license plate in order to be legally +operated on public streets; it does not include snowmobiles. The term +"trailer" includes any vehicle designed for transporting property or +passengers on its own structure and for being drawn by a self-propelled +vehicle. + +(e) + +The property owner's or tenants' first violation of this section shall be a +misdemeanor. The principal occupant of the property shall be responsible for +compliance with subsections (a) through (c) of this section. The records of +the city water department indicating the person responsible for payment of +city water bills shall constitute prima facie evidence of the identity of the +principal occupant. Such evidence may be rebutted by a lease or a property +owner's + +sworn statement which indicates the primary occupant of the property. The +owner of the property, according to the records of the tax assessor, shall be +responsible for compliance with subsection (d) of this section. + +(Prior Code, § 74-523) + + * Sec. 78-612. - Driveway and parking area standards for single-family and two-family residences. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +The driveway outside the public right-of-way will be limited to the width of +the garage plus ten feet, or a maximum of 20 feet in width if no garage +exists, or the maximum width of the garage for three stall garages or larger. + +(b) + +Driveways and parking areas shall be at least five feet from property lines, +except for the access to the street. Additional driveway and parking area +setbacks may be required from public rights-of-way and to avoid encroaching +into existing public drainage and utility easements. + +(c) + +Parking areas shall not be constructed in the front yard, except driveways. + +(d) + +The minimum driveway width in the public right-of-way shall be 12 feet. The +maximum driveway width in the public rights-of-way shall be the width of the +main garage plus four feet, not to exceed 24 feet. The curb returns (radii or +tapers) for the access to the street (driveway apron) are not included in the +driveway width. + +(e) + +Shared driveways are allowed, provided that property owners sharing the +driveway have easements and agreements relating to cross access and +maintenance. Shared driveways do not need to meet the five foot setback +required under subsection (b) of this section along the shared property line. + +(f) + +Driveway aprons shall be concrete, at least six inches thick, installed over a +Class V base a minimum of four inches thick upon a prepared, approved +subgrade, at least three feet wide from the back of the street curb. Where a +sidewalk exists, the driveway apron shall be constructed through the sidewalk. +The sidewalk portion of the driveway shall meet ADA cross grade standards. + +(g) + +The driveway entrance at the gutter line shall be constructed in a manner that +does not interfere with street drainage. + +(h) + +Driveways and parking areas shall be concrete, bituminous, brick pavers or +similar hard surface. Concrete driveways and parking areas shall be a minimum +of four inches thick installed over a Class V base a minimum of four inches +thick upon a prepared, approved subgrade. Bituminous driveways and parking +areas shall be a minimum of 2½ inches thick, installed over a Class V base a +minimum of four inches thick upon a prepared, approved subgrade. + +(i) + +Driveways on improved single- or two-family residential properties existing on +or before October 1, 1992, shall be paved with asphalt, concrete, brick, or +similar surface at such time as a building permit may be taken for either +remodeling or improvements costing more than $5,000.00. + +(j) + +New driveways shall be constructed in such a way as to provide positive +stormwater drainage from the garage or parking area to the street or an +approved stormwater drainage area. + +(k) + +Each single-family or duplex property is entitled to only one driveway from a +public right-of-way unless it can be demonstrated that an additional driveway +improves traffic safety/circulation for the general public. + +(Prior Code, § 74-524) + + * Sec. 78-613. - Standards for driveways and parking areas serving multifamily, commercial, industrial and nonresidential uses. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Driveway location is subject to review for traffic impacts such as volume +generated, adjacency to stop signs, speed of cross traffic, noise, and the +applicant's operating schedule. + +(b) + +Where a lot abuts two or more public rights-of-way, the city may require +access to be from the least traveled right-of-way if such least traveled +right-of-way does not direct traffic through a residential area. + +(c) + +In cases where a driveway serves a property not within the city jurisdictional +boundary, a joint powers agreement for maintenance and improvements to the +roadway must be in place before permission will be granted to allow access to +the adjacent street. The city reserves the right to reject or restrict any +proposal to allow access from city streets to multifamily, commercial, +industrial or nonresidential uses located in adjoining cities. + +(d) + +The operator of a principal building or use shall maintain parking and loading +areas, driveways, and yard areas in a neat and orderly manner. + +(e) + +Curbing. + +(1) + +All driveway areas and parking areas which are accessory to multifamily, +commercial, industrial or nonresidential developments shall be bounded by +concrete curb and gutter of a minimum of B612 design. + +(2) + +Driveway areas and parking areas which are accessory to low-use development +shall be bounded by concrete curb and gutter a minimum of B612 design on the +portions of such areas which front on a public right-of-way extending to the +wall. Concrete curb and gutter or curb only may be required of any other +driving or parking areas where necessary for drainage or traffic control. The +term "low-use development" shall include churches, parks, private clubs and +similar uses. + +(3) + +The city may exempt curbing where the city has approved future expansion of +the parking lot or to enhance traffic circulation where there are adjoining +lots. + +(4) + +Poured-in-place concrete traffic safety islands may be required to maintain a +safe and orderly flow of traffic within the parking lot. + +(5) + +Curb cuts and ramps for the handicapped shall be installed as required by +state law. + +(f) + +Driveways and parking areas shall be concrete, bituminous, brick pavers or +similar hard surface material. Section design shall be submitted for review +and approval of the city engineer. Driveway approach panels shall be a minimum +of eight inches thick to the right-of-way line. + +(g) + +The maximum width of a driveway shall be 30 feet. + +(h) + +Driveway aprons shall be concrete, at least eight inches thick, over a Class V +base a minimum of four inches thick upon a prepared, approved subgrade, at +least three feet wide from the back of the street curb of the street. Where a +sidewalk exists, the driveway apron shall be constructed through the sidewalk. +The sidewalk shall be replaced with at least an eight-inch-thick concrete +portion of the driveway and shall meet ADA cross grade standards. + +(Prior Code, § 74-525) + + * Secs. 78-614—78-644. - Reserved. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + * DIVISION 3. - TELECOMMUNICATION TOWERS + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + + + * Sec. 78-645. - Purpose and intent. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +In order to accommodate the communication needs of residents and businesses +while protecting the public health and safety, and general welfare of the +community, the city council finds the regulations of this division are +necessary to: + +(1) + +Facilitate the provision of wireless telecommunication services to the +residents and businesses of the city; + +(2) + +Minimize adverse visual effects of wireless telecommunication towers through +careful design and siting standards; + +(3) + +Avoid potential damage to adjacent properties from wireless telecommunication +tower failure through structural standards and setback requirements; and + +(4) + +Maximize the use of existing and approved towers, buildings and structures to +accommodate new wireless telecommunication antennas to reduce the number of +towers needed to serve the community. + +(b) + +This division is intended to regulate wireless telecommunication towers and is +not intended to regulate other types of towers, such as audio and television +antennas, residential satellite dishes or public safety transmitters. + +(Prior Code, § 74-541) + + * Sec. 78-646. - Definitions. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following words, terms and phrases, when used in this division, shall have +the meanings ascribed to them in this section, except where the context +clearly indicates a different meaning: + +Antenna means any structure or device used for the purpose of collecting or +transmitting electromagnetic waves, including, but not limited to, directional +antennas, such as panels, microwave dishes, and satellite dishes, and omni- +directional antennas, such as whip antennas. + +Co-location means the placement of wireless telecommunication antennas by two +or more service providers on a tower, building or structure. + +Federal Communications Commission (FCC) means the federal administrative +agency, or lawful successor, authorized to regulate and oversee +telecommunications carriers, services and providers on a national level. + +Guyed tower means a tower that is supported, in whole or in part, by wires and +ground anchors. + +Lattice or self-supported tower means a tower, erected on the ground, which +consists of metal crossed strips or bars to support antennas and related +equipment. + +Monopole tower means a single, self-supported pole-type tower, tapering from +the base to the top and supporting a fixture designed to hold one or more +antennas. + +Multi-user tower means a tower to which is attached the antennas of more than +one service provider or governmental entity. + +Protected residential property means any property within the city that meets +both of the following requirements: + +(1) + +The property is zoned R-1, R-2, or R-3 and the property may or may not also +have a planned unit development overlay classification; and + +(2) + +The property is designated on the comprehensive plan land use map as low- +density residential, medium-density residential or high-density residential. + +Public utility means persons, corporations, or governments supplying gas, +electric, transportation, water, or landline telephone service to the general +public. For the purpose of this division, wireless telecommunication service +facilities shall not be considered public utility uses and are defined +separately. + +Service provider means any individual or entity which provides wireless +telecommunication services. + +Single-user tower means a tower to which is attached only the antennas of a +single service provider, although the tower may be designed to accommodate the +antennas of multiple users as required in this division. + +Tower means any ground- or roof-mounted pole, spire, structure, or combination +thereof, including supporting lines, cables, wires, braces, and masts intended +primarily for the purpose of mounting or supporting an antenna, or an antenna +for wireless telecommunication purposes which is taller than 15 feet, +including roof antennas. + +Wireless telecommunication services means licensed commercial wireless +telecommunications services, including cellular, personal communication +services (PCS), specialized mobilized radio (SMR), enhanced specialized +mobilized radio (ESMR), paging, and similar services that are marketed to the +general public. + +(Prior Code, § 74-542) + + * Sec. 78-647. - Effect of division on existing towers and antennas. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Antennas and towers in existence as of November 7, 1997, which do not conform +or comply with this division are subject to the following provisions: + +(1) + +Towers may continue in use for the purpose now used and as now existing but +may not be replaced or structurally altered without complying in all respects +with this division. + +(2) + +If such towers are hereafter damaged or destroyed due to any reason or cause +whatsoever, the tower may be repaired and restored to its former use, location +and physical dimensions upon obtaining a building permit therefor, but without +otherwise complying with this section; however, if the cost of repairing the +tower to its former use, physical dimensions, and location would be 50 percent +or more of the cost of a new tower of like kind and quality, then the tower +may not be repaired or restored except in full compliance with this division. + +(Prior Code, § 74-543) + + * Sec. 78-648. - Application; building permits; fees; and inspections. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Application. Applications for approval to construct towers shall include +information as required in section 78-35. In addition to the information +required elsewhere in this division, applications for towers shall include the +following supplemental information: + +(1) + +A report from a licensed professional engineer which: + +a. + +Describes the tower height and design, including a cross section and +elevation; + +b. + +Documents the height above grade for all potential mounting positions for +collocated antennas and the minimum separation distances between antennas; + +c. + +Describes the tower's capacity, including the number and type of antennas it +can accommodate; + +d. + +Documents what steps the applicant will take to avoid interference with +established public safety telecommunications; + +e. + +Includes an engineer's stamp and registration number; and + +f. + +Includes other information necessary to evaluate the application. + +(2) + +A letter of intent committing the tower owner and the owner's successors to +allow the shared use of the tower if an additional user agrees in writing to +meet reasonable terms and conditions for shared use. + +(3) + +Applications requiring conditional use permits shall be subject to the +requirements set forth in section article IV, division 2 of this chapter, +excepting section 78-110. + +(b) + +Building permits. + +(1) + +It is unlawful for any person to erect, construct in place, place or re-erect, +replace, or repair any tower without first making application to the building +inspections department and securing a building permit therefor as provided in +this subsection (b). + +(2) + +The applicant shall provide at the time of application sufficient information +to indicate that construction, installation, and maintenance of the antenna +and tower will not create a safety hazard or damage to the property of other +persons. + +(3) + +Only one tower shall exist at any one time on any one parcel of protected +residential property as defined in section 78-646. + +(4) + +Building permits are not required for: + +a. + +Adjustment or replacement of the elements of an antenna array affixed to a +tower or antenna, provided that replacement does not reduce the safety factor. + +b. + +Antennas or towers erected temporarily for test purposes, for emergency +communication, or for broadcast remote pickup operations. Temporary antennas +shall be removed within 72 hours following installation. + +(5) + +Before issuance of a building permit, the following information shall be +submitted by the applicant: + +a. + +Proof that the proposed tower complies with regulations administered by the +federal aviation administration; and + +b. + +A report from a state-licensed professional engineer which demonstrates the +tower's compliance with structural and electrical standards. + +(6) + +Any city cost of testing or verification of compliance shall be borne by the +applicant. + +(c) + +Fee. The fee to be paid is that prescribed by the council. + +(d) + +Inspections. Towers may be inspected by an official of the building department +to determine compliance with original construction standards. Deviation from +original construction for which a permit is obtained constitutes a violation +of this section. Notice of violations will be sent by registered mail to the +owner of the tower and the property upon which it is located, who will have 30 +days from the date notification is issued to make repairs. Upon completion of +the repairs, the owner shall notify the building inspector that the repairs +have been made. + +(Prior Code, § 74-544) + + * Sec. 78-649. - Permitted and conditionally permitted towers. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Permitted towers. The following towers are permitted in all zoning districts +if in compliance with the performance standards set forth in section 78-650: + +(1) + +Towers located in the following locations: + +a. + +Church sites, when camouflaged as steeples or bell towers; + +b. + +Park sites, when compatible with the nature of the park; and + +c. + +Government, school, utility and institutional sites. + +(2) + +Wall- or roof-mounted towers. + +(b) + +Tower as conditional use. Towers, other than those listed in subsection (a) of +this section, are permitted in all zoning districts upon issuance of a +conditional use permit as follows: Commercial towers other than those listed +in subsection (a)(2) of this section. + +(c) + +Conditional use permit standards. The following standards apply to a +conditional use permit for a tower: + +(1) + +The site must comply with the performance standards set forth in section +78-650. + +(2) + +No employees of the service providers shall be located on the site on a +permanent basis. Employees may be on the site to perform periodic maintenance. + +(3) + +If the proposed tower is located in a residential district, documentation must +be included in the application that demonstrates that the tower cannot +reasonably be located in a commercial or industrial district. + +(4) + +Existing on-site vegetation shall be preserved to the maximum amount +practicable. + +(5) + +No outdoor storage shall be permitted on the tower site. + +(d) + +Towers located within boundaries to blend in with surrounding environment. +Towers located within the boundaries of the Mississippi River Corridor +Critical Overlay District shall be designed and constructed to blend in with +the surrounding environment. + +(Prior Code, § 74-545) + + * Sec. 78-650. - Performance standards. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +All towers erected within the city must conform to the applicable performance +standards contained in this section. + +(1) + +Co-location requirements. All towers erected, constructed or located within +the city shall comply with the following requirements: A proposal for a new +tower shall not be approved unless the city council finds that the wireless +telecommunications equipment planned for the proposed tower cannot be +accommodated on an existing or approved tower, building or structure within a +one mile radius, except that the radius shall be one-half mile for towers +between 80 and 120 feet and one-quarter mile for towers under 80 feet of the +proposed tower due to one or more of the following reasons: + +a. + +The planned equipment would exceed the structural capacity of the existing or +approved tower or building, as documented by a licensed professional engineer, +and the existing or approved tower cannot be reinforced, modified, or replaced +to accommodate planned or equivalent equipment at a reasonable cost. + +b. + +The planned equipment would cause interference materially impacting the +usability of other existing equipment at the tower or building as documented +by a licensed professional engineer and the interference cannot be prevented +at a reasonable cost. + +c. + +Existing or approved towers or buildings within the radius cannot accommodate +the planned equipment at a height necessary to function reasonably as +documented by a licensed professional engineer. + +d. + +Other unforeseen reasons that make it infeasible to locate the planned +telecommunications equipment upon an existing or approved tower or building. + +(2) + +Construction and maintenance of towers. + +a. + +Tower and antenna design requirements. Proposed or modified towers and +antennas shall meet the following design requirements: + +1\. + +Towers and antennas shall be designed to blend into the surrounding +environment through the use of color and camouflaging architectural treatment, +except in instances where the color is dictated by federal or state +authorities such as the Federal Aviation Administration. + +2\. + +Towers shall be of a monopole design unless the city council determines that +an alternative design would better blend in to the surrounding environment. +Lattice tower designs may be allowed to facilitate co-location. + +3\. + +The use of guyed towers is prohibited. Towers must be self-supporting without +the use of wires, cables, beams or other designs. + +4\. + +The base of the tower shall occupy no more than 500 square feet and the top of +the tower shall be no larger than the base. + +b. + +Tower construction requirements. All antennas and towers erected, constructed, +or located within the city, and all wiring therefor, shall comply with the +following requirements: + +1\. + +All applicable provisions of this division must be met. + +2\. + +Towers shall be certified by a state-licensed professional engineer to conform +to current structural standards and wind loading requirements of the state +building code and the Electronics Industry Association. + +3\. + +With the exception of necessary electric and telephone service and connection +lines approved by the city, no part of any antenna or tower, nor any lines, +cables, equipment or wires or braces in connection with either shall at any +time extend across or over any part of the right-of-way, public street, +highway, sidewalk, or property line. + +4\. + +Towers and associated antennas shall be designed to conform with accepted +electrical engineering methods and practices and to comply with the provisions +of the National Electrical Code. + +5\. + +All signal and remote control conductors of low energy extending substantially +horizontally above the ground between a tower or antenna and a structure, or +between towers, shall be at least eight feet above the ground at all points, +unless buried underground. + +6\. + +Every tower affixed to the ground shall be protected to discourage climbing of +the tower by unauthorized persons. + +7\. + +All towers shall be constructed to conform with the requirements of the +occupational safety and health administration. + +8\. + +Antennas and towers shall not be erected on any protected residential property +as defined in section 78-646 in violation of the following restrictions: + +(i) + +Notwithstanding the provisions of this division, the required setback for +antennas and towers not rigidly attached to a building shall be equal to the +height of the antenna and tower. Those antennas and towers rigidly attached to +a building, and whose base is on the ground, may exceed this required setback +by the amount equal to the distance from the point of attachment to the +ground. + +(ii) + +No tower shall be in excess of a height equal to the distance from the base of +the antenna and tower to the nearest overhead electrical power line, which +serves more than one dwelling or place of business, less five feet. + +(iii) + +Metal towers shall be constructed of, or treated with, corrosive-resistant +material. Wood poles shall be impregnated with rot resistant substances. + +(3) + +Tower setbacks. Towers shall conform with each of the following minimum +setback requirements: + +a. + +Towers shall be set back from any property line a minimum distance equal to +the height of the tower. + +b. + +A tower's setback may be reduced or its location in relation to a public +street varied, at the sole discretion of the city council, to allow +integration of a tower into an existing or proposed structure such as a church +steeple, light standard, power line support device or similar structure. + +c. + +The minimum distance to a residential structure shall be the height of the +tower plus ten feet. + +d. + +The tower or associated accessory structures shall not encroach upon any +public easements. + +e. + +The setback shall be measured from a point on the base of the tower located +nearest the property line to the actual property line. + +(4) + +Height. The height of towers shall be determined by measuring the vertical +distance from the tower's point of contact with the ground or rooftop to the +highest point of the tower, including all antennas or other attachments. When +towers are mounted upon other structures, the combined height of the structure +and tower must meet the height restrictions of section 78-557. + +(5) + +Height limitations for towers. + +a. + +In all protected residential property, towers, including antennas and other +attachments, shall not exceed a maximum height of 60 feet. + +b. + +In residential property other than protected residential property, the maximum +height of any tower, including antennas and other attachments, shall not +exceed 90 feet. + +c. + +In all nonresidential zoning districts, the maximum height of any tower, +including antennas and other attachments, shall not exceed 150 feet. + +d. + +Exceptions to the provisions of this subsection (5) shall be as follows: +Multi-user towers may exceed the height limitations of section 78-557 by up to +20 feet. + +e. + +Noncompliance of characteristics of antennas and towers created by application +of this division shall not in any manner limit the legal use of the property, +nor in any manner limit the repair, maintenance, or reconstruction of a +noncomplying antenna or tower; however, in no instance shall the degree on +noncompliance be increased except as otherwise permitted by this chapter. + +(6) + +Tower lighting. Towers shall not be illuminated by artificial means and shall +not display strobe lights unless such lighting is specifically required by the +Federal Aviation Administration or other federal or state authority for a +particular tower. When incorporated into the approved design of the tower, +light fixtures used to illuminate ball fields, parking lots, or similar areas +may be attached to the tower. + +(7) + +Signs and advertising. The use of any portion of a tower for signs other than +warning or equipment information signs is prohibited. + +(8) + +Accessory utility buildings. All utility buildings and accessory structures to +a tower shall be architecturally designed to blend in with the surrounding +environment and shall meet the minimum setback requirements of the underlying +zoning district. Ground-mounted equipment shall be screened from view by +suitable vegetation, except where a design of nonvegetative screening better +reflects and complements the architectural character of the surrounding +neighborhood. + +(9) + +Abandoned or unused towers or portions of towers. Abandoned or unused towers +or portions of towers shall be removed as follows: + +a. + +All abandoned or unused towers and associated facilities shall be removed +within 12 months of the cessation of operations at the site unless a time +extension is approved by the zoning administrator. In the event that a tower +is not removed within 12 months of the cessation of operations at a site, the +tower and associated facilities may be removed by the city and the costs of +removal assessed against the property. + +b. + +Unused portions of towers above a manufactured connection shall be removed +within six months of the time of antenna relocation. The replacement of +portions of a tower previously removed shall require the issuance of a new +conditional use permit. + +(10) + +Antennas mounted on roofs, walls, and existing structures. The placement of +wireless telecommunication antennas on roofs, walls, and existing towers may +be approved by the zoning administrator, provided the antennas meet the +requirements of this division, after submittal of a final site and building +plan as specified in section 78-36, and a report prepared by a licensed +professional engineer indicating the existing structure or tower's suitability +to accept the antenna and the proposed method of affixing the antenna to the +structure. Complete details of all fixtures and couplings, and the precise +point of attachment shall be indicated. Accessory equipment for wall- or roof- +mounted antennas must be located within the principal building or, if located +on the rooftop, must be enclosed. + +(11) + +Interference with public safety telecommunications. No new or existing +telecommunications service shall interfere with public safety +telecommunications. The city may require that all applications for new service +be accompanied by an intermodulation study which provides a technical +evaluation of existing and proposed transmissions and indicates all potential +interference problems. Before the introduction of new service or changes in +existing service, telecommunication providers shall notify the city at least +ten calendar days in advance of such changes and allow the city to monitor +interference levels during the testing process. + +(12) + +Lights and other attachments. No antenna or tower on any protected residential +property as defined in section 78-646 shall have affixed or attached to it in +any way, except during time of repair or installation, any lights, reflectors, +flashers, or other illuminating device, except as required by the Federal +Aviation Agency or the Federal Communications Commission, nor shall any tower +have constructed thereon, or attached hereto, in any way, any platform, +catwalk, crow's nest, or like structure, except during periods of construction +or repair. + +(13) + +Security fencing. Towers shall be provided with security fencing to prevent +unauthorized entry. + +(Prior Code, § 74-546) + + * Secs. 78-651—78-663. - Reserved. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + * DIVISION 4. - WIND ENERGY CONVERSION SYSTEM + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + + + * Sec. 78-664. - Purpose and intent. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +This division is established to regulate the installation and operation of +wind energy conversion systems (WECS) within the city, not otherwise subject +to siting and oversight by the state under the Minnesota Power Plant Siting +Act, M.S.A. § 216E.001 et seq. + +(Prior Code, § 74-560) + + * Sec. 78-665. - Definitions. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following words, terms and phrases, when used in this division, shall have +the meanings ascribed to them in this section, except where the context +clearly indicates a different meaning: + +Commercial WECS means a WECS of 40 kilowatts (KW) or more in total name plate +generating capacity. + +Fall zone means the area defined as the furthest distance from the tower base, +in which a tower will collapse in the event of a structural failure. + +Feeder line means any power line that carries electrical power from one or +more wind turbines or individual transformers associated with individual wind +turbines to the point of interconnection with the electric power grid; in the +case on interconnection with the high voltage transmission systems, the point +of interconnection shall be the substation serving the WECS. + +Meteorological tower means towers that are erected primarily to measure wind +speed and directions plus other data relevant to siting a WECS. For the +purposes of this division, the term "meteorological towers" does not mean +towers and equipment used by airports, the state department of transportation, +or other similar applications to monitor weather conditions. + +Nacelle means the part of the WECS that contains the key components of the +wind turbine, including the gearbox, yaw system and the electrical generator. + +Noncommercial WECS means a WECS of less than 40 kilowatts (KW) in total name +plate generating capacity. + +Rotor diameter means the diameter of the circle described by the moving rotor +blades. + +Substations means any electrical facility designed to convert electricity +produced by a wind turbine to a voltage greater than 35,000 volts (35 +kilovolts) for interconnection with high voltage transmission lines. + +Total height means the highest point, above ground level, reached by a rotor +tip or any other part of the WECS. + +Tower means vertical structures that support the electrical generator, rotor +blades, or meteorological equipment. + +Tower height means the total height of the WECS, exclusive of the rotor +blades. + +Transmission line means those electrical power lines that carry voltages of at +least 69,000 volts (69 kilovolts) and are primarily used to carry electric +energy over medium to long distances rather than directly interconnecting and +supplying electric energy to retail customers. + +Wind energy conversion system (WECS) means an electrical generating facility +comprised of one or more wind turbines and accessory facilities, including, +but not limited to, power lines, transformers, substations and meteorological +towers that operate by converting the kinetic energy of wind into electrical +energy. The energy may be used on-site or may be distributed into the +electrical grid. + +Wind turbine means any piece of electrical generating equipment that converts +the kinetic energy of blowing wind into electrical energy through the use of +airfoils or similar devices to capture the wind. + +(Prior Code, § 74-561) + + * Sec. 78-666. - Application; process; building permits; fees; inspections. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Application. Applications for approval to construct a commercial WECS shall +include the following information: + +(1) + +The names of the project applicant. + +(2) + +The names of the property owner. + +(3) + +The legal description and address of the project. + +(4) + +A description of the project, including the type, name plate generating +capacity, tower height, rotor diameter, and means of interconnecting with the +electrical grid. + +(5) + +The proposed site layout, including the location of property lines, wind +turbines, electrical wires, interconnection points with the electrical grid, +and all related accessory structures. The site layout shall include distances +and shall be drawn to scale. + +(6) + +An engineer's certification. + +(7) + +Documentation of land ownership or legal control of the property. + +(8) + +The latitude and longitude of individual wind turbines. + +(9) + +A USGS topographical map, or map with similar date, of the property and +surrounding area, including any other WECS within ten rotor diameters of the +proposed WECS. + +(10) + +The location of wetlands, scenic and natural areas within 1,320 feet of the +proposed WECS. + +(11) + +An acoustical analysis. + +(12) + +A Federal Aviation Administration (FAA) permit application, if applicable. + +(13) + +The location of all known communication towers within two miles of the +proposed WECS. + +(14) + +A decommissioning plan. + +(15) + +A description of potential impacts on any nearby WECS and wind resources on +adjacent properties. + +(b) + +Process. WECS applications will be processed under the procedures for +applicable approvals contained within this division. + +(c) + +Building permits. + +(1) + +It is unlawful for any person to erect, construct in place, place or re-erect, +replace, or repair any tower without first making application to the building +inspections department and securing a building permit therefor as required in +this subsection (c). + +(2) + +The applicant shall provide, at the time of application, sufficient +information to indicate that construction, installation and maintenance of the +WECS will not create a safety hazard or damage to the property of other +persons. + +(3) + +Only one tower shall exist at any one time on any one property. + +(4) + +Before issuance of a building permit, the following information shall be +submitted by the applicant: + +a. + +Proof that the proposed tower complies with regulations administered by the +Federal Aviation Administration; + +b. + +A report from a state-licensed professional engineer that demonstrates the +WECS compliance with structural and electrical standards; + +c. + +A conditional use permit approved by the city. + +(5) + +Any city cost of testing or verification of compliance shall be borne by the +applicant. + +(d) + +Fees. The fees to be paid shall be as prescribed by the city council. + +(e) + +Inspections. Any WECS may be inspected by an official of the building +department to determine compliance with original construction standards. +Deviation from the original construction for which a permit is obtained +constitutes a violation of this section. Notice of violations will be sent by +registered mail to the owner of the WECS and the property owner upon which the +WECS is located, who will have 30 days from the date notification is issued to +make repairs. Upon completion of the repairs, the owner/applicant shall notify +the building official that the repairs have been made. + +(Prior Code, § 74-562) + + * Sec. 78-667. - Conditionally permitted and prohibited WECS. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Conditionally permitted WECS. Commercial WECS are permitted in all zoning +districts, except as noted in subsection (b) of this section, upon issuance of +a conditional use permit, and are subject to the provisions of section 78-668. + +(b) + +Prohibited WECS. All WECS are prohibited in the environmental overlay +districts, Mississippi National River Recreation Area (MNRRA) and the Rum +River Wild and Scenic District and are prohibited in the floodplain or shore +land areas. Noncommercial WECS are prohibited in all areas of the city. + +(Prior Code, § 74-563) + + * Sec. 78-668. - Performance standards. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Safety design standards. + +(1) + +Engineering certification. For all WECS, the manufacture's engineer or another +qualified engineer shall certify that the turbine, foundation and tower design +of the WECS is within accepted professional standards, given local soil and +climate conditions. + +(2) + +Clearance. Commercial WECS rotor blades must maintain at least 15 feet of +clearance between their lowest point and the ground. + +(3) + +Rotor safety. Each commercial WECS shall be equipped with both a manual and an +automatic braking device capable of stopping the WECS operation in high winds +(40 miles per hour or greater). + +(4) + +Lightning protection. Each commercial WECS shall be grounded to protect +against natural lightning strikes in conformance with the National Electrical +Code. + +(5) + +Warnings. For all commercial WECS, signs shall be posted on the tower, +transformer and substation warning of high voltage, stating the manufacturer's +name and listing an emergency phone number. + +(b) + +Standards. + +(1) + +Total height. + +a. + +Commercial WECS shall have a total height of no more than 150 feet. + +b. + +WECS shall not be roof-mounted. + +(2) + +Tower configuration. + +a. + +All towers that are part of a WECS, except meteorological towers, shall be +installed with a tubular, monopole type tower. + +b. + +Meteorological towers may be guyed. + +(3) + +Setbacks. + +_Expand_ + ++----+-----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------+ +| | 0 | 1 | 2 | +|----+-----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------| +| 0 | nan | Commercial WECS | Meteorological Towers | +| 1 | Property lines | 1.1 times the total height plus ten feet | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | +| 2 | Neighboring dwellings | 1¼ times the total height | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | +| 3 | Road rights-of-way | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | +| 4 | Other rights-of-way | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | +| 5 | Other structures | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | The lesser of the fall zone, as certified by a professional engineer, plus ten feet or 1.1 times the total height | +| 6 | Other existing WECS | To be determined through the CUP review based on relative size of existing and proposed WECS, alignment of WECS relative to predominant winds, topography, extent of wake interference on existing WECS, and other setbacks required; may be waived for multiple turbine projects | nan | ++----+-----------------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+-------------------------------------------------------------------------------------------------------------------+ + + + +(4) + +Color and finish. + +a. + +All wind turbines and towers that are part of a WECS shall be white, grey or +another nonreflective, nonobtrusive color. + +b. + +Finishes shall be matte or nonreflective. + +(5) + +Lighting. Lighting, including lighting intensity and frequency of strobe, +shall adhere to but not exceed requirements established by Federal Aviation +Administration (FAA) permits and regulations. No additional lighting, other +than building security lighting, is permitted. + +(6) + +WECS sites. The design of the buildings and related structures shall, to the +extent reasonably possible, use materials, colors, textures, screening and +landscaping that will blend the WECS to the natural setting and then existing +environment. + +(7) + +Signs. The manufacturer's or owner's company name or logo may be placed on the +nacelle of the WECS. No other signage, other than as required in this +division, shall be permitted. + +(8) + +Feeder lines. All communications and feeder lines, equal or less than 34.5 +kilovolts in capacity, installed as part of a WECS shall be buried where +reasonably feasible. Feeder lines installed as part of a WECS shall not be +considered an essential service. + +(9) + +Waste disposal. All solid and hazardous wastes, including, but not limited to, +crates, packaging materials, damaged or worn parts, as well as used oils and +lubricants, shall be removed from the site promptly and disposed of in +accordance with all applicable local, state and federal regulations. + +(10) + +Maximum vibration and shadow flicker. + +a. + +No WECS shall produce vibrations through the ground that are humanly +perceptible beyond the property on which it is located. + +b. + +Commercial WECS shall include a shadow flicker analysis study with the +application submission. + +(11) + +Discontinuation and decommissioning. A WECS shall be considered a discontinued +use after one year without energy production, unless a plan is developed and +submitted to the city outlining the steps and schedule for returning the WECS +to service. + +a. + +All WECS and accessory buildings shall be removed in their entirety, including +all footings and foundations, within 90 days of the discontinuation of use. + +b. + +Each commercial WECS shall submit a decommissioning plan outlining the +anticipated means and cost of removing the WECS at the end of its serviceable +life or upon becoming a discontinued use. The plan shall also identify the +financial resources that will be available to pay for the decommissioning and +removal of the WECS and accessory facilities. The decommissioning plan shall +be submitted as part of the conditional use permit application. + +c. + +The city may require financial surety in the form of a cash escrow, +irrevocable letter of credit or performance bond to ensure that +decommissioning of the commercial WECS is completed. + +(Prior Code, § 74-564) + + * Sec. 78-669. - Other applicable standards. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +(a) + +Noise. All WECS shall comply with the MPCA and city standards for noise. + +(b) + +Electrical codes and standards. All WECS and accessory equipment and +facilities shall comply with the National Electrical Code and other applicable +standards. + +(c) + +FederalAviation Administration (FAA). All WECS shall comply with FAA standards +and permit requirements. + +(d) + +Building code. All WECS shall comply with the state building code as adopted +by the state and the city. + +(e) + +Interference. + +(1) + +The applicant shall minimize or mitigate interference with electromagnetic +communications, such as radio, telephone, microwaves, or television signals +caused by WECS. + +(2) + +The applicant shall notify all communication tower operators within two miles +of the proposed WECS location upon application to the city for a permit to +operate a WECS. + +(3) + +No WECS shall be constructed so as to interfere with public safety +telecommunications. + +(Prior Code, § 74-565) + + * Secs. 78-670—78-689. - Reserved. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + * DIVISION 5. - TRAFFIC ANALYSIS + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + + + + * Sec. 78-690. - Purpose and intent. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Streets and thoroughfares are an essential component of the city's street +network and are necessary to accommodate the community's health, safety and +welfare and ability to grow and develop in a logical and financially +responsible manner. The purpose and intent of this division is to ensure that: + +(1) + +Traffic volumes and traffic operations generated by platting, re-platting, +rezoning, a change in use, or new development will not prevent the city from +implementing its then planned street system improvements. + +(2) + +Traffic volumes and traffic operations generated by platting, re-platting, +rezoning, a change in use, or new development will not negatively impact a +community's existing street system and traffic operations or create safety +hazards. + +(3) + +New plats, land that is rezoned, or re-platted, a change in use, and new +development will be served and supported by an adequate network of streets and +thoroughfares. Necessary and desirable public rights-of-way for off-site, +abutting and internal thoroughfares will be provided to support new +development at the time of platting, rezoning, re-platting or development of +the land. + +(4) + +Driveway accessibility or on-site circulation plans for a change in use or new +development will not significantly impact or create safety of traffic +operations on adjacent public streets, or prevent the safe and convenient +circulation of on-site traffic operations. + +(5) + +Parking demand generated by platting, rezoning, re-platting, a change in use, +or new development will be adequately addressed on-site or in off-street, +satellite parking facilities. + +(6) + +Opportunities to reduce travel demand or efficiently manage travel demand will +be investigated and implemented. + +(Prior Code, § 74-575) + + * Sec. 78-691. - Definitions. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following words, terms and phrases, when used in this division, shall have +the meanings ascribed to them in this section, except where the context +clearly indicates a different meaning: + +Change in use means a use which may create traffic patterns that substantially +differ from traffic patterns of the existing approved use of a building or +land, based upon a consideration of the following: + +(1) + +Modifications to existing improvements or construction of new improvements. + +(2) + +The hours, days or seasons during which a use operates. + +(3) + +The number of employees or staff, occupants, visitors or other persons using +the land, buildings or structures. + +(4) + +The number of employees or staff, occupants, visitors or other persons using +the land, buildings, or structures. + +(5) + +The amount or nature of traffic, parking, shipping or deliveries associated +with the use on the premises. + +Daily trip ortrips per day means the number of trips a particular land use +will generate within a 24-hour period. + +Intersection level of service (LOS) means a measure of delay vehicles will +experience at intersections. + +Peak hour trips means the number of trips typically between 7:00 a.m. and 9:00 +a.m. (a.m. peak) and between 4:00 p.m. and 6:00 p.m. (p.m. peak) Monday +through Friday, or as may be specifically attributable to the building or land +based upon its particular use. + +Roadway LOS means a measure of the volume of traffic a roadway carries in +relation to its capacity to carry traffic. + +Traffic impact assessment (TIA) means a study that looks at current and fore- +cast future conditions after a development is implemented. TIAs focus on trip +generation at the site, trip distributions to/from the site, traffic +assignments to/from driveways serving the site, the street adjacent to the +site, driveways (number and locations) serving the site, traffic control +mechanisms at the site driveways, driveway and adjacent intersection levels of +service (LOS), on-site circulation, and parking generation, supply and +configuration. + +Traffic impact study (TIS) means a more rigorous study that takes into account +everything in the TIA and additional conditions that are distant from the site +and that occur under specific development scenarios, such as existing +conditions, fore-cast no-build conditions, and fore-cast build conditions. + +(Prior Code, § 74-576) + + * Sec. 78-692. - Items to address in traffic analysis. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +Based upon a review of the TIA or TIS and other applicant supplied data, the +planning commission and city council will determine if the proposed rezoning, +platting, re-platting, change of use, or new development plans meet the +following: + +(1) + +The plans are consistent with the city's then existing planned improvements +and will not prevent the city from moving forward with its plans. + +(2) + +The plans will not create safety hazards. + +(3) + +The plans provide for adequate accessibility between the development and the +street system and an adequate on-site circulation system. + +(4) + +The plans provide for adequate on-site parking (or satellite parking) as +determined by applicable city ordinance. + +(5) + +The plans include reasonable approaches to reduce or manage travel demand. + +(Prior Code, § 74-577) + + * Sec. 78-693. - Traffic impact assessment (TIA). + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +A traffic impact assessment is required if a rezoning, re-platting, or change +of use generates between 50 and 99 peak hour trips per peak direction +(entering or leaving), above the trip generation for the use as it existed +prior to the rezoning, re-platting, or change of use, determined by the +greater of the then existing actual trip generation or the latest edition of +the Institute of Transportation Engineers' (ITE) trip generation for the +existing use, or another method approved by the city; or upon the platting, +re-platting or new development of vacant land if the proposed use is expected +to generate between 500 and 749 daily trips. + +(Prior Code, § 74-578) + + * Sec. 78-694. - Traffic impact statement. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +A traffic impact statement (TIS) is required to be submitted, rather than a +TIA, if the criteria of section 78-291 is met and the peak hour trips per peak +direction exceed 100 or the daily trips exceed 749\. + +(Prior Code, § 74-579) + + * Sec. 78-695. - Elements of traffic analysis. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following table lists the major elements to include in each of the two +types of traffic analysis: + +ELEMENTS TO INCLUDE IN TRAFFIC ANALYSIS + +_Expand_ + ++----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------+----------------------+ +| | 0 | 1 | 2 | +|----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------+----------------------| +| 0 | Element Included in Traffic Analysis | Traffic Impact Assessment | Traffic Impact Study | +| 1 | Impact Analysis | nan | nan | +| 2 | Describe characteristics and features of adjacent street (street and intersection geometrics; traffic control devices; turn, general traffic, parking, and bike lanes; sight distance; pedestrian accommodations and facilities, etc.) | ✓ | ✓ | +| 3 | Pre-development existing conditions along adjacent street and at adjacent intersections (LOS) | ✓ | ✓ | +| 4 | Opposing driveway locations and conditions (LOS) | ✓ | ✓ | +| 5 | Study area and future road summary | nan | ✓ | +| 6 | Understanding of the development program and operations for the proposed development | ✓ | ✓ | +| 7 | Trip generation for on-site uses | ✓ | ✓ | +| 8 | Trip distribution analysis | ✓ | ✓ | +| 9 | Background traffic growth | nan | ✓ | +| 10 | Traffic assignments to driveways and adjacent intersections | ✓ | ✓ | +| 11 | Site driveway intersection capacity (LOS) | ✓ | ✓ | +| 12 | Future conditions at nearby intersections (LOS) | nan | ✓ | +| 13 | Mitigation identifications and analysis | ✓ | ✓ | +| 14 | Site Analysis | nan | nan | +| 15 | Number and location of driveways serving the site | ✓ | ✓ | +| 16 | Access design and queuing | ✓ | ✓ | +| 17 | On-site circulation | ✓ | ✓ | +| 18 | Other Analysis | nan | nan | +| 19 | Planned and programmed roadway improvements | nan | ✓ | +| 20 | Planned and approved developments in vicinity of site | nan | ✓ | +| 21 | Traffic impacts of planned/approved developments | nan | ✓ | +| 22 | Traffic analysis (LOS and queue analysis) at distant intersections and roadway segments for: | nan | nan | +| 23 | Future no-build condition | nan | ✓ | +| 24 | Future build condition | nan | ✓ | +| 25 | Travel demand management and transportation system management techniques (as appropriate) | nan | ✓ | ++----+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+---------------------------+----------------------+ + + + +(Prior Code, § 74-580) + + * Sec. 78-696. - Required information. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following information must be included in the traffic impact assessment: + +(1) + +Background. + +a. + +Name of development and developer. + +b. + +Development location and zoning classification. + +c. + +Description of study area, setting and features of the area where the +development is proposed to be implemented. + +d. + +Description of proposed development program and operations (design year and +opening of development, peak days of week and peak times of day, typical +vehicle occupancy, describe patrons as appropriate). + +e. + +Identify other factors that will bear on traffic (planned/programmed roadway +improvements, other developments proposed/approved for the area, etc.). + +(2) + +Site plan. + +a. + +Identify use (residential, commercial, office, institutional, industrial, +etc.). + +b. + +A detailed description of the proposed use. + +c. + +A detailed description of the site. + +d. + +A description of the building footprint and how it sits on the proposed site. + +e. + +The number and location of access driveways, clearly labeled, and assessed +relative to this chapter. + +f. + +Parking supply, assessed relative to the chapter. + +(3) + +Traffic assessment results. The traffic study must include: + +a. + +Assessment of existing conditions. + +1\. + +Identify and describe adjacent intersections serving the site. + +2\. + +Quantify peak hour turning movements. + +3\. + +LOS at adjacent intersections. + +b. + +Assessment of post development conditions. + +1\. + +Trip generation, trip distribution, traffic assignment to driveways and +adjacent intersections. + +2\. + +LOS at driveways and at adjacent intersections. + +(4) + +Summary of findings. + +a. + +Observations. + +b. + +Conclusions. + +c. + +Recommendations. + +(Prior Code, § 74-581) + + * Sec. 78-697. - Specific elements of traffic impact study. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The following items include information to be included for specific elements +of the traffic impact study: + +(1) + +Background. + +a. + +Name of development and developer. + +b. + +Development location and zoning classification. + +c. + +Description of study area, setting and features of the area where the +development is proposed to be implemented. + +d. + +Description of proposed program and operations (design year and opening of +development, peak days of week and peak times of day, typical vehicle +occupancy, describe patrons as appropriate). + +e. + +Identify other factors that will bear on traffic (planned/programmed roadway +improvements, other developments proposed/approved for the area, etc.). + +(2) + +Site plan. + +a. + +Identify use (residential, commercial, office, institutional, industrial, +etc.). + +b. + +A detailed description of the proposed use. + +c. + +A detailed description of the site. + +d. + +A description of the building footprint and how it sits on the proposed site. + +e. + +The number and location of access driveways, clearly labeled, and assessed +relative to this chapter. + +f. + +Parking supply, assessed relative to this chapter. + +g. + +Describe bicycle parking supply, assess relative to this chapter. + +(3) + +Existing traffic conditions. + +a. + +Define the existing condition. + +b. + +Show existing two-way daily traffic and comment on roadway LOS. + +c. + +Identify existing driveways adjacent to or opposing proposed driveways, +describe any traffic operations issues, recommend and test mitigations to +address issues. + +d. + +Show existing peak hour turning movements at intersections that will be +affected by the proposed development. + +e. + +Conduct existing intersection capacity analysis and report existing LOS and +storage issues. + +f. + +Recommend and test mitigation measures to ensure either minimum LOS D under +existing conditions and adequate storage, or a LOS no worse than the lowest +LOS for the affected intersection at any time. + +(4) + +Future no-build conditions. + +a. + +Define the no-build condition, including any significant changes in land use +in the vicinity of the proposed development and any changes in the roadway +network that will have taken place since the existing condition. + +b. + +Conduct analysis to fore-cast no-build, two-way daily traffic and comment on +roadway LOS. + +c. + +Re-visit existing driveways adjacent to or opposing proposed driveways, +describe traffic operations issues relative to fore-cast two-way daily +traffic, recommend and test mitigations to address issues. + +d. + +Conduct analysis to fore-cast no-build peak hour intersection turning +movements. + +e. + +Conduct fore-cast no-build intersection capacity analysis and report LOS and +storage issues. + +f. + +Recommend and test mitigation measures to ensure either minimum LOS D or a LOS +no worse than the lowest LOS for the affected intersection at any time under +fore-cast no-build conditions and adequate storage. + +(5) + +Future build conditions. + +a. + +Define the build condition. + +b. + +Conduct analysis to quantify the effects of the build condition: + +1\. + +Trip generation analysis using the latest edition of trip generation, +institute of transportation engineers, account for pass-by and multi-purpose +trips, provide credit for transit trips. + +2\. + +Trip distribution analysis using an approved approach (population within +traffic analysis zones, households within traffic analysis zones, two-way +daily traffic on roadways serving the site, etc.). + +3\. + +Assign traffic to driveways and roadways serving the site in accordance with +outcomes from the trip distribution analysis. + +c. + +Revisit existing driveways adjacent to or opposing proposed driveways, +describe traffic operations issues relative to fore-cast two-way daily +traffic, recommend and test mitigations to address issues. + +d. + +Add assigned traffic to no-build condition intersection turning movements to +derive build condition intersection turning movements. + +e. + +Conduct fore-cast build intersection capacity analysis and report LOS and +storage issues. + +f. + +Recommend and test mitigation measures to ensure either minimum LOS D or a LOS +no worse than the lowest LOS for the affected intersection at any time under +fore-cast no-build conditions and adequate storage. + +g. + +Quantify fore-cast build condition, two-way daily traffic and comment on LOS. + +(6) + +On-site circulation. + +a. + +Describe location of access routes, relative to driveways and front and rear +doors of buildings. + +b. + +Describe locations of dumpsters and delivery/loading docks and how service +vehicles will circulate and maneuver. + +(7) + +On-site parking. + +a. + +Describe proposed parking supply. + +b. + +Assess proposed supply against required parking supply in this chapter. + +c. + +Describe rationalization if there is a discrepancy between proposed and +required supplies. Quantify parking generation (demand) per the latest edition +of the ITE, ULI, or other recognized source. + +d. + +Recommend an approach to resolve discrepancy. + +e. + +Describe proposed bicycle parking supply relative to this Code and how +bicycles will circulate to bike parking racks. + +(8) + +Travel demand management. Identify, as appropriate, approaches to reduce +travel demand and how they might be applied. + +a. + +Transit. + +b. + +Carpool. + +c. + +Employer sponsored vanpool. + +d. + +Employer incentives. + +e. + +Bike and bike facilities. + +f. + +Pedestrian and pedestrian facilities. + +(9) + +Summary of findings. + +a. + +Observations. + +b. + +Conclusions. + +c. + +Recommendations. + +(Prior Code, § 74-582) + + * Sec. 78-698. - Exception to the regulations within this division. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +The city recognizes that there is very little that can be done to expand +capacity and improve traffic operations beyond incremental operational changes +(adjusting signals, adding operational control devices, (i.e., stop signs) in +the downtown. As such, a change of use for existing properties in the downtown +area where parking is not required does not require a traffic analysis. Staff +may perform a traffic trip generation analysis to monitor the need for +improvements in the street system. New development in this area, however, must +meet the standards of this division to the extent applicable. + +(Prior Code, § 74-583) + + * Secs. 78-699—78-719. - Reserved. + +__ + + * __Share Link + * __Print + * __Download (docx) + * __Email + * __Compare + +__Share Link to section __Print section __Download (Docx) of sections __Email +section __Compare versions + +__Secs. 78-524—78-553. - Reserved. ARTICLE X. - NONCONFORMING USES AND +DIMENSIONALLY SUBSTANDARD STRUCTURES __ + +__View All Meetings ×Close + +__Close + +×Close + +__Close + +__ + +__Close + +__ + +__Add Note __ + +__ + + * __ + + * __ + +Home + + * __ + +Codes + + * __ + +Ordinances + + * __ + +Documents + + * __ + +Links + + * www.ci.anoka.mn.us + * Municode + * Municode Library + * Order a physical copy + * Terms of use + +Copyright © 2024 **Municode.com** + +×Close + +#### MunicodeNEXT Terms of Use + +1 + +### Navigation Menu + +Navigate between Codes, individual ordinances and documents (related documents +such as minutes and agendas). You will also find links back to the +municipality or content creator's website. + +Next + +Close + diff --git a/tests/data/Atlantic New Jersey.txt b/tests/data/Atlantic New Jersey.txt new file mode 100644 index 00000000..7c056a65 --- /dev/null +++ b/tests/data/Atlantic New Jersey.txt @@ -0,0 +1,673 @@ +This website uses cookies to ensure you get the best experience on our +website. Learn more + +Got it! + +City of Linwood, NJ + +Atlantic County + +Login + +By using eCode360 you agree to be legally bound by the Terms of Use. If you do +not agree to the Terms of Use, please do not use eCode360. + +Home + +Help + +Jump to... + +Search + +Pin open the table of contents + +Table of Contents + +Open the table of contents + +Table of Contents + + * Part I: Administrative Legislation + * Ch 1 General Provisions + * Ch 7 Claims Approval + * Ch 10 Court, Municipal + * Ch 14 Defense and Indemnification + * Ch 17 Economic Development Committee + * Ch 20 Education, Board of + * Ch 23 Emergency Management, Office of + * Ch 26 Environmental Commission + * Ch 30 Fire Department + * Ch 41 Land Use Procedures + * Ch 43 Length of Service Awards Program + * Ch 47 Officers and Employees + * Ch 52 Personnel and Personnel Procedures + * Ch 52A Employee Manual + * Ch 56 Police Department + * Ch 59 Public Works Department + * Ch 59A Purchasing + * Ch 60 Records Management + * Ch 61 Recreation, Board of + * Ch 66 Salaries + * Ch 69 Seal + * Ch 71 Shade Tree Commission + * Part II: General Legislation + * Ch 78 Affordable Housing + * Ch 80 Alarm Systems + * Ch 83 Alcoholic Beverages + * Ch 86 Animals + * Ch 94 Bingo and Raffles + * Ch 99 Brush, Grass and Weeds + * Ch 106 Buildings, Unsafe + * Ch 109 Cannabis + * Ch 111 Canvassing and Soliciting + * Ch 115 Clothing Donation Bins + * Ch 119 Construction Codes, Uniform + * Ch 122 Curfew + * Ch 124 Development Fees + * Ch 128 Drug-Free Zones + * Ch 132 Energy-Creating Devices + * Ch 136 (Reserved) + * Ch 140 Fees + * Ch 145 Firearms + * Ch 149 Fire Limits + * Ch 152 Fire Prevention + * Ch 155 Flood Damage Prevention + * Ch 159 Garage Sales, Yard Sales and Private Sales + * Ch 165 Hazardous Materials + * Ch 172 Licensed Occupations + * Ch 175 Littering + * Ch 179 Mercantile Licenses + * Ch 183 Nuisances + * Ch 187 Obscene Materials + * Ch 191 Parental Responsibility + * Ch 194 Parking for Handicapped + * Ch 197 Parks and Recreation Areas + * Ch 200 Peace and Good Order + * Ch 203 Portable Toilets + * Ch 205 Property Maintenance + * Ch 209 Railroads + * Ch 212 Rental Property + * Ch 216 Satellite Earth Station Antennas + * Ch 221 Sewers + * Ch 222 (Reserved) + * Ch 224 Site Clearing + * Ch 226 Short-Term Rentals + * Ch 228 Smoking + * Ch 232 Soil Removal + * Ch 235 Solid Waste + * Ch 238 Stormwater Management + * Ch 241 Streets and Sidewalks + * Ch 244 (Reserved) + * Ch 247 Swimming Pools + * Ch 251 Taxation + * Ch 255 Television and Radio Systems, Damaging of + * Ch 257 Towing + * Ch 259 Trees + * Ch 263 Vehicles and Traffic + * Ch 266 Vehicles, Motor-Driven + * Ch 269 Vehicles, Unlicensed + * Ch 270 Vehicles, Used + * Ch 273 Renewable Energy Systems + * § 273-1 Title. + * § 273-2 Authority. + * § 273-3 Purpose. + * § 273-4 Definitions. + * § 273-5 Standards. + * § 273-6 Permit requirements. + * § 273-7 Abandonment. + * § 273-8 Zoning permit procedure. + * § 273-9 Unlawful acts; exemption. + * § 273-10 Administration and enforcement. + * § 273-11 Violations and penalties. + * Ch 277 Zoning + * Appendix + * Ch A284 (Reserved) + * Ch A285 Cable Television Franchise + * Derivation Table + * Ch DT Derivation Table + * Disposition List + * Ch DL Disposition List + +Code + +New Laws (0) Index + +Print + +Email + +Download + +Share + +Get Updates + +arrow_back + +City of Linwood, NJ / Part II: General Legislation + +Chapter 273 Renewable Energy Systems + +arrow_forward + +[HISTORY: Adopted by the Common Council of the City of Linwood 10-13-2010 by +Ord. No. 13-2010; amended in its entirety 9-25-2013 by Ord. No. 17-2013. +Subsequent amendments noted where applicable.] + +GENERAL REFERENCES + +Energy-creating devices — See Ch. 132. + +Satellite earth station antennas — See Ch. 216. + +Zoning — See Ch. 277. + +§ 273-1 Title. + +§ 273-2 Authority. + +§ 273-3 Purpose. + +§ 273-4 Definitions. + +§ 273-5 Standards. + +§ 273-6 Permit requirements. + +§ 273-7 Abandonment. + +§ 273-8 Zoning permit procedure. + +§ 273-9 Unlawful acts; exemption. + +§ 273-10 Administration and enforcement. + +§ 273-11 Violations and penalties. + +§ 273-1 Title. + +This chapter may be referred to as the "Renewable Energy Systems Ordinance." + +§ 273-2 Authority. + +This chapter is adopted pursuant to the authority of the Common Council of the +City of Linwood. + +§ 273-3 Purpose. + +It is the purpose of this regulation to promote the safe, effective and +efficient use of small wind, solar, and other renewable energy systems +installed to reduce the on-site consumption of utility-supplied electricity. +In addition, these regulations are designed to consider aesthetics in the use, +placement and design of renewable energy systems. + +§ 273-4 Definitions. + +As used in this chapter, the following terms shall have the meanings +indicated: + +FALL ZONE + +The potential fall area for the small wind energy system. It is measured by +using 110% of the total height as the radius around the center point of the +base of the tower. + +METEOROLOGICAL TOWER or MET TOWER + +A structure designed to support the gathering of wind energy resources data, +and includes the tower, base plate, anchors, guide wires and hardware, +anemometers, wind direction vanes, booms to hold equipment anemometers and +vanes, data logger, instrument wiring, and any telemetry devices that are used +to monitor or transmit wind speed and wind flow characteristics over a period +of time for either instantaneous wind information or to characterize the wind +resources at a given location. + +OWNER + +The individual or entity that intends to own and operate the renewable energy +system in accordance with this chapter. + +RENEWABLE ENERGY SYSTEM + +Any structure or installation, such as a small wind energy system, solar- +collecting array, or geothermal system, which is designed and intended to +produce energy from natural forces such as wind, sunlight or geothermal heat. + +ROTOR DIAMETER + +The cross-sectional dimension of the circle swept by the rotating blades of +the wind powered energy generator. + +SHADOW + +The outline created on the surrounding area by the sun shining on the small +wind energy system. + +SMALL WIND ENERGY SYSTEM + +A wind generator and all associated equipment, including the base, blade, +foundation, nacelle, rotor, tower, transformer, vane, wire, inverter, +batteries or other component necessary to fully utilize the wind generator +which is used to generate electricity, that: + +A. + +Has a nameplate capacity of 100 kilowatts or less; + +B. + +Has decibel levels that do not exceed 50 decibels (dBA) measured at the +closest property line shared with a buildable lot; and + +C. + +Is as high as necessary to capture the wind energy resource at 120 feet. + +SOLAR ENERGY SYSTEM + +An accessory to the main structure and/or use which comprises of a combination +of solar collector(s) and ancillary solar equipment used to generate +electricity primarily for consumption on the property on which the system is +located, or where multiple consumers or exceptional circumstances exist, on an +adjoining property. + +TOTAL HEIGHT OF SMALL WIND ENERGY SYSTEM + +The vertical distance from the ground to the tip of the wind generator blade +when the tip is at its highest point. + +TOWER + +A monopole, freestanding, or guyed structure that supports a wind generator. + +WIND GENERATOR + +The equipment that converts energy from the wind into electricity. This term +includes the rotor, blades and associated mechanical and electrical conversion +components necessary to generate, store and/or transfer energy. + +§ 273-5 Standards. + +A renewable energy system shall be erected, constructed or permitted only if +it complies with the following requirements: + +A. + +A renewable energy system shall not be the principal use on the site. + +B. + +Economic benefit for wind energy systems. The applicant shall demonstrate +through a cost/benefit analysis that the project is economically feasible and +sustainable. + +C. + +Location; setbacks; height. + +(1) + +A renewable energy system, except for roof-mounted solar-collecting arrays, +must meet the setback requirements for principal structures for the zoning +district in which the system is located. + +[Amended 2-24-2021 by Ord. No. 3-2021] + +(2) + +Solar energy systems are only permitted on the roof of the principal +structure. The solar panels shall not exceed a height of eight inches from the +rooftop. In no event shall the placement of the solar panels result in a total +height including building and panels than what is permitted in the zoning +district which they are located for the principal building. + +[Amended 2-24-2021 by Ord. No. 3-2021] + +(3) + +In the case of a flat roof, solar panels may extend up to 10 feet above the +roofline (so they can be angled to maximize production), shall not be visible +from the street, and shall comply with the maximum height limit of the zoning +district. + +(4) + +A wind tower for a small wind energy system shall be set back a distance equal +to the fall zone from: + +(a) + +Any public right-of-way, unless written permission is granted by the +government entity with jurisdiction over the road right-of-way. + +(b) + +Any overhead utility lines. + +(c) + +All property lines. + +(d) + +All travelways, to include but not limited to driveways, parking lots or +sidewalks. + +(e) + +The setback shall be measured from the center of the tower's base. + +(f) + +Guy wires used to support the tower are exempt from the small wind energy +setback requirements. + +D. + +Renewable energy systems shall be designed to blend into the architecture of +the building to the extent possible. Solar roof shingles and all exterior +plumbing and electrical lines must be painted and/or coated to match the color +of the adjacent walls and/or roofing material. All visible exterior plumbing +and electrical lines must not be installed in any portion of the front of the +property. Aluminum trim, if used and visible, should be anodized or otherwise +color-treated to blend into the surroundings. + +E. + +Clearing. Clearing of natural vegetation shall be limited to that which is +necessary for the construction, operation and maintenance of the renewable +energy system and as otherwise prescribed by applicable law. + +F. + +Signs. There shall be no signs that are visible from any public road posted on +a small wind generator system or any associated building, except for the +manufacturer's or installer's identification, appropriate warning signs or +owner identification. + +G. + +Utility notification and interconnection. The small wind energy system that +connects to the electric utility shall comply with the New Jersey's Net +Metering and Interconnection Standards for Class I Renewable Energy Systems at +N.J.A.C. 14:4-9. + +H. + +Additional Standards for Wind Turbines: + +(1) + +Wind turbines may only be attached to freestanding or guy-wired monopole +towers. Lattice towers are explicitly prohibited. + +(2) + +The tower height shall not exceed 150 feet. + +(3) + +The applicant shall provide evidence that the proposed tower height does not +exceed the height recommended by the manufacturer of the wind turbine. + +(4) + +Sound level. The small wind energy system shall not exceed 50 decibels using +the A scale (dBA), as measured at the property line, except during short-term +events such as severe windstorms and utility outages. + +(5) + +Shadowing/flickers. Small wind energy systems shall be sited in a manner that +does not result in significant shadowing or flicker impacts. The applicant has +the burden of proving that this effect does not have significant adverse +impact on neighboring or adjacent uses either through siting or mitigation. + +(6) + +All ground-mounted electrical and control equipment shall be labeled and +secured to prevent unauthorized access. + +(7) + +The tower shall be designed and installed so as not to provide step bolts, a +ladder, or other publicly accessible means of climbing the tower, for a +minimum height of eight feet above the ground. + +(8) + +Lighting. A small wind energy system shall not be artificially lighted unless +such lighting is required by the Federal Aviation Administration. + +(9) + +Visual impacts. It is inherent that small wind energy systems may pose some +visual impacts due to the tower height needed to access the wind resources. +The purpose of this section is to reduce the visual impacts without +restricting the owner's access to the wind resources. + +(a) + +The applicant shall demonstrate that through the project site planning that +the small wind energy system's visual impacts will be minimized for +surrounding neighbors and the community. This may include, but not be limited +to, information regarding site selection, turbine design or appearance, +buffering, and screening of ground-mounted electrical and control equipment. +All electrical conduits shall be underground. + +(b) + +Appearance, color and finish. The wind generator and the tower shall remain +painted or finished in the color or finish that was originally applied by the +manufacturer's or installer's identification, unless a different color of +finish is approved in the zoning approval. + +(10) + +Aviation. The small wind energy system shall be built to comply with all +applicable Federal Aviation Administration and state regulations. + +(11) + +Met tower. A met tower shall be permitted under the same standards, permit +requirements, restoration requirements and permit procedures as a small wind +energy system. Met towers shall be permitted on a temporary basis not to +exceed three years. + +§ 273-6 Permit requirements. + +A. + +Permit. A zoning permit shall be required for the installation of a renewable +energy system. + +B. + +Documents. The zoning permit application shall be accompanied by a plot plan +prepared by a licensed New Jersey surveyor which includes the following: + +(1) + +Property lines and physical dimensions of the property; + +(2) + +Location, dimensions and types of existing structures on the property; + +(3) + +Location of the proposed renewable energy system and all associated equipment; + +(4) + +The setback requirements as outlined in this chapter; + +(5) + +The right-of-way of any public road that is contiguous with the property; + +(6) + +Any overhead utility lines; + +(7) + +Renewable energy system specifications, including manufacturer and model, and +the manufacturer's specification sheet in sufficient detail to allow for a +determination that the manner of installation conforms to the National +Electric Code; + +(8) + +Sound level analysis prepared by the wind turbine manufacturer or qualified +engineer; + +(9) + +Evidence of compliance or nonapplicability with Federal Aviation +Administration requirements; + +(10) + +The application shall meet all the requirements of a building permit, include +standard drawings and an engineering analysis, and certification by a +professional mechanical, structural or civil engineer as required by the +Construction Official; + +(11) + +For a small wind energy system, tower foundation and tower blueprints or +drawings. The foundation shall be signed and sealed by a professional +engineer, registered in the State of New Jersey, certifying that the +foundation complies with all of the standards set forth for safety and +stability in all applicable codes in effect in the State of New Jersey; + +(12) + +Estimated costs of physically removing the renewable energy system to comply +with surety standards; + +(13) + +The applicant must post a surety of the approved cost estimate and an escrow +in an amount equal to 5% of the cost estimate for engineering inspections. + +C. + +Fees. The application for a zoning permit for a renewable energy system must +be accompanied by the fee required. + +D. + +Expiration. A permit issued pursuant to this chapter shall expire if: + +(1) + +The renewable energy system is not installed and functioning within 24 months +from the date the permit is issued; or + +(2) + +The renewable energy system is out of service or otherwise unused for a +continuous twelve-month period. + +§ 273-7 Abandonment. + +A. + +A renewable energy system that is out of service for a continuous twelve-month +period will be deemed abandoned. + +B. + +The Zoning Officer may issue a notice of abandonment to the owner of a +renewable energy system that is deemed to have been abandoned. The notice +shall be sent return receipt requested. + +C. + +The owner shall have the right to respond to the notice of abandonment within +30 days from notice receipt date. + +D. + +If the owner provides information that demonstrates the renewable energy +system has not been abandoned, the Zoning Officer shall withdraw the notice of +abandonment and notify the owner that the notice has been withdrawn. + +E. + +If the Zoning Officer determines that the renewable energy system has been +abandoned, the owner of the renewable energy system shall remove the system at +the owner's sole expense within three months after the owner receives the +notice of abandonment. + +F. + +If the owner fails to remove the renewable energy system in the time allowed +under Subsection E above, the Zoning Officer may pursue legal action to have +it removed at the owner's expense, or if the Zoning Officer facilitates the +removal, all costs, fees and interest shall be payable by the owner and shall +be a lien on the property until satisfied. + +§ 273-8 Zoning permit procedure. + +A. + +An owner shall submit an application to the Zoning Officer for a permit for a +renewable energy system. + +B. + +The Zoning Officer shall issue a permit or deny the application as consistent +with the Municipal Land Use Law. + +§ 273-9 Unlawful acts; exemption. + +A. + +It is unlawful for any person to construct, install, or operate a renewable +energy system that is not in compliance with this chapter. + +B. + +Renewable energy systems installed prior to the adoption of this chapter are +exempt from the requirements in this chapter, except for the provisions in § +273-7 regarding abandonment. + +§ 273-10 Administration and enforcement. + +A. + +This chapter shall be administered by the Zoning Administrator or other +official as designated. + +B. + +The Zoning Officer may enter any property for which a permit has been issued +under this chapter to conduct inspections to determine whether the conditions +stated in the permit have been met. + +C. + +The Zoning Officer may issue orders to abate any violation of this chapter, +may issue a citation for any violations and may refer any violations of this +chapter to legal counsel for enforcement. + +§ 273-11 Violations and penalties. + +A. + +Any person adjudged guilty of a violation of the provisions of this chapter +shall, upon conviction thereof, be punished by a fine not exceeding $1,500 at +the discretion of the Municipal Judge of the City of Linwood. + +B. + +Nothing in this section shall be construed to prevent the City of Linwood from +requiring abatement and using any other lawful mean to enforce this chapter. + +Privacy Policy Desktop View Responsive View Terms of Use Powered by General +Code + diff --git a/tests/data/Barber Kansas.pdf b/tests/data/Barber Kansas.pdf new file mode 100644 index 00000000..524bf672 Binary files /dev/null and b/tests/data/Barber Kansas.pdf differ diff --git a/tests/data/Decatur Indiana.pdf b/tests/data/Decatur Indiana.pdf new file mode 100644 index 00000000..f336401c Binary files /dev/null and b/tests/data/Decatur Indiana.pdf differ diff --git a/tests/data/Hamlin South Dakota.pdf b/tests/data/Hamlin South Dakota.pdf new file mode 100644 index 00000000..fd5df58d Binary files /dev/null and b/tests/data/Hamlin South Dakota.pdf differ diff --git a/tests/data/Whatcom.txt b/tests/data/Whatcom.txt new file mode 100644 index 00000000..b3d653f4 --- /dev/null +++ b/tests/data/Whatcom.txt @@ -0,0 +1,365 @@ + + + + + + + + Chapter 20.14 WIND ENERGY SYSTEMS + + + + + + + + +
+

Chapter 20.14
WIND ENERGY SYSTEMS

+

Sections:

+ +

20.14.010    Purpose.

+ +

20.14.030    Applicability.

+ +

20.14.040    Regulatory framework.

+ +

20.14.050    General requirements for SWES and WES.

+ +

20.14.060    Sound levels, modeling and measurement.

+ +

20.14.070    Safety.

+ +

20.14.100    Abandonment, insurance, and decommissioning for WES.

+ +

20.14.110    Federal, state and local requirements.

+ +

20.14.010 Purpose.

+

The purpose of this chapter is to regulate the installation and operation of wind energy conversion systems in Whatcom County for private landowners, subject to reasonable restrictions. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2008-043 § 1, 2008).

+ +

20.14.030 Applicability.

+

(1) The requirements set forth in this chapter shall govern the siting of wind energy conversion systems used to generate mechanical or electrical energy to perform work, and which may be connected to the utility grid pursuant to Chapter 80.60 RCW (Net Metering of Electricity), and serve as an independent source of energy, or serve as part of a hybrid system.

+ +

(2) The requirements of this chapter shall apply to all small wind energy systems (SWES) and wind energy systems (WES) proposed after October 10, 2008 (the effective date of the ordinance codified in this chapter). Any SWES/WES for which a required permit has been properly issued prior to the effective date of the ordinance codified in this chapter shall not be required to meet the requirements of this chapter; provided, however, that any such pre-existing SWES/WES that is not producing energy for a continuous period of 12 months shall meet the requirements of this chapter prior to recommencing production of energy. No modification that increases the height of the system or increases the system output more than 25 percent shall be allowed without full compliance with this chapter. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2008-043 § 1, 2008).

+ +

20.14.040 Regulatory framework.

+

.041 Permits and Zoning.

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

System Type

+ +
+

Zones Allowed In

+ +
+

Required Permit

+ +
+

Meteorological tower

+ +
+

All districts

+ +
+

Permitted

+ +
+

SWES

+ +
+

 

+ +
+

 

+ +
+

 • with a total height of 200 feet or less

+ +
+

All districts

+ +
+

Permitted

+ +
+

 • with a total height greater than 200 feet

+ +
+

All districts

+ +
+

Administrative use permit2

+ +
+

WES

+ +
+

 

+ +
+

 

+ +
+

 

+ +
+

Heavy Impact Industrial, Light Impact Industrial

+ +
+

Administrative use permit2

+ +
+

 

+ +
+

Agriculture, Rural Forestry, and Commercial Forestry

+ +
+

Conditional use permit3

+ +
+

1    SWES, WES and meteorological towers are required to be in compliance with WCC Title 15, Buildings and Construction, and acquire the necessary building permits.

+ +

2    Administrative use permit, WCC 22.05.028.

+ +

3    Conditional use permit, WCC 22.05.026.

+ +

.042 Principal or Accessory Use.

+ +

A SWES/WES may be considered either as a principal or accessory use. A different existing use or an existing structure on the same lot shall not preclude the installation of a SWES/WES or a part of such facility on such lot. Any SWES/WES that is constructed and installed in accordance with the provisions of this chapter shall not be deemed to constitute the expansion of a nonconforming use or structure. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2011-013 § 2 Exh. B, 2011; Ord. 2008-043 § 1, 2008).

+ +

20.14.050 General requirements for SWES and WES.

+

.051 Visual Appearance – Lighting – Power Lines.

+ +

(1) Wind turbines shall be painted a nonreflective, nonobtrusive color such as the manufacturer’s default color option or a color that conforms to the environment and architecture of the community, unless Federal Aviation Administration (FAA) standards require otherwise. The director may require a photo of a SWES/WES, of the same model as that proposed in the landowner’s application, adjacent to a building or some other object illustrating scale (e.g., manufacturer’s photo).

+ +

(2) No SWES/WES shall be artificially lighted, except to the extent required by the Federal Aviation Administration (FAA) or other applicable authority.

+ +

(3) No SWES/WES shall be used for displaying any advertising except for reasonable identification of the manufacturer or operator of the wind turbine.

+ +

(4) Electrical controls, control wiring, and power lines shall be wireless or underground, except where SWES/WES wiring is brought together for connection to the transmission or distribution network adjacent to that network, and except that in the Agricultural Zone the minimum installation depth for electrical controls, control wiring, and power lines is 48 inches below finish grade.

+ +

(5) The road access to the proposed site must be rated to carry an axle load sufficient to bear the weight of all materials, vehicles, and equipment delivered to the site.

+ +

(6) The compatibility of the foundation, tower, and generating unit (including rotor and rotor-related equipment) shall be certified in writing by a professional engineer licensed in Washington State. The engineer shall certify compliance with established engineering practices and compliance with all applicable adopted codes and regulations. For all SWES/WES, the manufacturer’s engineer or another qualified engineer shall certify that the turbine, foundation, and tower design of the SWES/WES are compatible and within accepted professional standards, given local design criteria per WCC Title 15.

+ +

(7) The electrical system design shall be certified in writing by an electrical engineer licensed in Washington State unless waived by the building official. All SWES/WES electrical systems shall comply with requirements per the Washington State Department of Labor and Industries and the current adopted edition of the National Electrical Code when and where applicable.

+ +

(8) All SWES/WES shall meet requirements per the applicable sections of WCC 20.80.630, et seq. (stormwater and drainage) for erosion control and stormwater management.

+ +

.052 Setback Requirements.

+ +

The following setback requirements shall apply to SWES/WES and meteorological towers. All setbacks are measured from the property lines of the property on which the project is located:

+ +

(1) Setbacks. Setbacks from property lines shall be as shown in the following table, measured to the outer edge of the base of the SWES/WES structure towers. Guy cables and other accessory support structures may be located within setback areas.

+ + + + + + + + + + + + + + + + + + +
+

System Size

+ +
+

Setback Requirement

+ +
+

SWES

+ +
+

1 times total height of SWES structure

+ +
+

WES

+ +
+

1,000 feet from a property line of any property in other than the HII district. If the neighboring property is in an HII district the setback is 1 times the total height.1

+ +
+

1 A reduction in setbacks may be approved if appropriate easements from neighboring property owners or appropriate mitigation acceptable to neighboring property owners is approved by the zoning administrator or hearing examiner and recorded against the applicable deed(s).

+ +

(2) Setbacks from Communication and Electrical Lines. Each SWES/WES shall be set back a distance no less than one times its total height, from any existing above-ground power line or telephone line.

+ +

(3) Setback from Other WES. A WES may not be placed such that it substantially disturbs the wind flow into another WES. A new WES may not be placed such that another nonparticipating WES falls within an egg-shaped exclusion zone around the new WES defined by an axis along the primary wind direction. In the upwind direction the exclusion zone shall have a semi-circular shape with a radius three times the rotor diameter of the new WES. In the downwind direction the exclusion zone shall have a semi-elliptical shape extending eight times the rotor diameter of the new WES along the axis downwind and extending three times the rotor diameter of the new WES in a direction perpendicular to the axis. In this way the new WES will be at least three of its rotor diameters behind, three to the side of, and eight in front of a pre-existing WES.

+ +

(4) For WES located within 1,000 feet of existing structures, permit applicants shall provide additional analysis of safety risks, including estimate of range for “ice throw” from spinning blades.

+ +

.053 Height Limits.

+ +

SWES with a total height taller than 200 feet must obtain an administrative use permit, except within the AG, CF and HII Zones. All SWES with a total height greater than 200 feet must provide in writing that the height requested is the minimum height necessary for the SWES to operate efficiently, and provide approved justification for the proposed height and analysis according to recognized industry standards. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2008-043 § 1, 2008).

+ +

20.14.060 Sound levels, modeling and measurement.

+

(1) During normal operation, the SWES/WES shall comply with the sound requirements of the zoning district in which it is located. The facility shall maintain sound levels at project boundaries that are under the maximum levels for the adjacent receiving properties based on the receiving properties’ environmental designation for noise abatement in accordance with state regulations. The facility shall at all times comply with applicable noise control regulations adopted by the Washington Department of Ecology or such other state agency with jurisdiction. The maximum sound level may be exceeded during short-term events, such as utility outages and storms.

+ +

(2) WES proponents shall provide a report by a qualified independent acoustical consultant approved by Whatcom County PDS and in accordance with standard industry best practices, that models the sound transmission of the proposed WES at the project property lines and indicates that the WES, when operated properly, will conform to the sound performance requirements of this chapter.

+ +

(3) Noise Complaints.

+ +

(a) If two or more complaints from different households are received within a two-week period regarding a particular WES located within one mile of the complainant’s properties, a sound measurement will be conducted by a qualified consultant approved by Whatcom County. The cost of the sound measurement shall be paid initially by the county. Measurements shall be conducted where the complaints were documented. If an evaluation shows that the WES is operating outside of its permitted sound performance standards, the operator will have 30 days to adjust the system(s) or terminate operations, and the owner/operator shall reimburse the county for the expense of sound measurement. If the WES is shown to be in compliance, the complainant shall reimburse the county for the cost of measurement.

+ +

(b) At the discretion of Whatcom County PDS, multiple complaints may be compiled for three months at a time and then a sound study conducted at all of the locations. No WES project shall be required to conduct more than two sound measurements at any one adjacent property per year unless the WES project has expanded and/or proven to be in violation of the sound performance standards. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2008-043 § 1, 2008).

+ +

20.14.070 Safety.

+

.071 General Provisions for SWES/WES.

+ +

(1) Wind turbine towers shall not provide step bolts or a ladder readily accessible to the public; any access bolts or ladders shall be a minimum height of 10 feet above ground level.

+ +

(2) All electrical equipment shall be safely and appropriately enclosed from unintentional access by means such as barrier fencing, equipment cabinetry or similar approved barriers. All access doors to wind turbine towers and electrical equipment shall remain locked except when access is necessary.

+ +

(3) Appropriate warning signage (e.g., electrical hazards) shall be placed on wind turbine towers, electrical equipment, and SWES/WES.

+ +

(4) Any SWES/WES found to be unsafe by the building official shall be repaired by the landowner and/or project owner to meet federal, state and local safety standards, according to the regulatory authority of the building official and applicable provisions per WCC Title 15.

+ +

.072 Blade Tip Clearance.

+ +

(1) The blade tip of any SWES shall, at its lowest point, have ground clearance of no less than 20 feet, as measured at the lowest point of the arc of the blades.

+ +

(2) WES shall, at its lowest point, have ground clearance of no less than 30 feet, as measured at the lowest point of the arc of the blades.

+ +

.073 Over-Speed Controls.

+ +

All SWES/WES shall be equipped with over-speed controls to limit rotation of blades to speed below the designed limits of the system. No changes or alterations from the certified design shall be permitted unless accompanied by a licensed professional engineer’s statement of certification.

+ +

.074 Flicker Analysis for WES.

+ +

A flicker analysis is required for all WES. The analysis shall include the duration and location of flicker potential for all buildings and for roadways within a one-mile radius of each turbine within a project. The applicant shall provide a site map identifying the locations of shadow flicker that may be caused by the project and the expected durations of the flicker at these locations from sunrise to sunset over the course of a year. The analysis shall account for topography but not for obstacles such as accessory structures and trees. Flicker at any building shall not exceed 30 hours per year within the analysis area. Flicker in excess of the limits established in this chapter shall be grounds for the director to order operational adjustments, which may include mitigation measures requiring cessation of operation during periods when flicker affects any building, for all noncompliant WES.

+ +

.075 Wildlife Protection for WES.

+ +

(1) Prior to permit approval, the applicant shall ensure compliance with Chapter 16.16 WCC (Critical Areas), including potential impacts to birds and bats, and providing documentation of compliance with Washington Department of Fish and Wildlife’s “Wind Power Guidelines” for project siting and operation to minimize take of listed species, migratory birds, raptors, and bats.

+ +

(2) The applicant shall assess and monitor proximate bird and bat habitats for activity prior to construction, and modify construction timing and activities to avoid impacts to these species.

+ +

(a) At a minimum, one raptor nest survey during breeding season within one mile of the project site should be conducted to determine the location and species of active nests potentially disturbed by construction activities, and to identify active and potentially active nest sites with the highest likelihood of impacts from the operation of the wind plant. A larger survey area (e.g., a two-mile buffer) is recommended if there is some likelihood of the occurrence of nesting state and/or federally threatened and endangered raptor species (e.g., ferruginous hawk, bald eagle, golden eagle).

+ +

(b)  A minimum of one full season of use surveys is recommended to estimate the use of the project area by birds and bats. This data should be used to refine impact analyses and help determine project design.

+ +

(3) Following project start-up, the applicant shall monitor the project for a minimum of one year to estimate bird and bat fatality rates using standard protocol. The applicant shall report bird fatalities observed for the life of the project to WDFW and USFWS on a quarterly basis. Additional monitoring may be required by the county. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2008-043 § 1, 2008).

+ +

20.14.100 Abandonment, insurance, and decommissioning for WES.

+

.101 Abandonment.

+ +

Absent notice of a proposed date of decommissioning, a WES project shall be considered abandoned when the project fails to operate for more than one year without the written approval of the director. The director shall determine in his/her decision what proportion of the project is inoperable for the project to be considered abandoned and shall notify the property owner. Within 120 days of receipt of notice of abandonment or within 120 days of providing notice of termination of operations to the county, the owner of a wind energy system must comply with the removal requirements in WCC 20.14.102. If the property owner/project owner fails to do so, the county shall have the authority to enter the property and physically remove the WES. Financial surety funds shall be used to pay for removal and restoration.

+ +

.102 Removal Requirements.

+ +

When a SWES or a WES is scheduled to be decommissioned, the project owner/property owner shall notify the county by certified mail of the proposed date of discontinued operations and plans for removal. Within 120 days of receipt of notice of abandonment or within 120 days of providing notice of termination of operations, the owner of a wind energy system must:

+ +

(1) Decommission any SWES, including removal of wind turbine, tower, and above-ground cabling and electrical components. Foundations and underground cabling need not be removed.

+ +

(2) Decommission any WES, including removal of wind turbines, tower, and above-ground cabling and electrical components, removal of all below-ground project elements to a depth of 36 inches, access roads, and any other associated facilities, unless the property owner requests in writing that the access roads or other facilities be retained.

+ +

(3) Remove all hazardous material from the property and dispose of the hazardous material in accordance with federal, state, and local law.

+ +

(4) In addition to removing the wind turbine generator, the owner shall restore the site by planting native or other approved vegetation to minimize erosion.

+ +

.103 Insurance.

+ +

For WES, proof of continuous liability insurance shall be submitted to Whatcom County indicating coverage for potential damages or injury to landowners, occupants, or other third parties. The required insurance is $2,000,000 aggregate and $1,000,000 per occurrence. Whatcom County shall be named on the liability policy as additional insured. The insurance carrier shall be instructed to notify all applicable governmental authorities of any delinquency in payment of premiums. The liability policy shall be endorsed to notify the county of any cancellation 30 days in advance. Failure to provide such insurances shall be considered abandonment and full and sufficient grounds for termination of the permit and disposal of the equipment and appurtenances as stated herein.

+ +

.104 Financial Surety.

+ +

(1) As a condition of WES permit approval, the applicant shall be required to provide a form of surety (i.e., post a bond, or establish an escrow account or other means) at the amount of 150 percent of the estimated full cost of project decommissioning, less the approved, documented salvage value of any applicable project materials and equipment, naming Whatcom County as the beneficiary, with 50 percent due prior to final project approval, 25 percent due within 12 months of the date of final project approval, and 25 percent due within 24 months of the date of final project approval, to cover costs of WES removal in the event the county must remove the facility. Nothing shall prevent the county from seeking reimbursement from the WES project owner. The project owner is responsible to the county for any costs related to decommissioning that exceed the amount of financial surety.

+ +

(2) As part of the decommissioning plan, the applicant shall submit a fully inclusive estimate of the costs associated with removal, accounting for reasonable salvage value of any applicable project materials and equipment, prepared by a qualified professional. The decommissioning plan shall provide that the decommissioning funds shall be reevaluated every five years from the date of substantial completion of the WES to ensure sufficient funds for decommissioning and, upon mutual agreement by the applicant and the county at that time, the amount of decommissioning funds shall be adjusted accordingly.

+ +

(3) Prior to permit issuance, the applicant shall provide the county with a copy of the financial surety device or another approved mechanism.

+ +

.105 Decommissioning Plan.

+ +

As part of the permit approval process, a decommissioning plan shall be provided that outlines the anticipated means and cost of removing WES at the end of their serviceable life or upon becoming a discontinued use. The cost estimates shall be made by a competent party, such as a professional engineer, a licensed contractor capable of decommissioning, or a person, firm, partnership, corporation, or other entity with suitable expertise or experience with decommissioning, as determined by the building official. The plan shall also identify financial surety to pay for the decommissioning and removal of the WES and accessory facilities. The plan shall also address road maintenance during and after the decommissioning. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2023-018 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012).

+ +

20.14.110 Federal, state and local requirements.

+

(1) SWES/WES shall comply with all current adopted Whatcom County codes and ordinances, including but not limited to WCC Titles 15, 16 and 23.

+ +

(2) SWES/WES must comply with regulations of the Federal Aviation Administration (FAA), along with the requirements of WCC 20.80.675 (Height limitations surrounding airports). If necessary, an applicant may be required to submit the following information for analysis of airspace obstructions in relation to WCC 20.80.675: mean sea level (MSL) of adjacent airports; MSL of proposed site; Euclidean distance from adjacent airports to proposed site; total elevation/height of SWES/WES structure.

+ +

(3) All SWES/WES electrical systems shall comply with requirements per the Washington State Department of Labor and Industries and the current adopted edition of the National Electrical Code (NEC) when and where applicable.

+ +

(4) All SWES/WES with the intention to tie to their respective utility provider’s grid system shall meet the requirements of Chapter 80.60 RCW, Net Metering of Electricity. (Ord. 2023-042 § 1 (Exh. A), 2023; Ord. 2012-041 § 1 (Exh. A), 2012; Ord. 2008-043 § 1, 2008. Formerly 20.14.080).

+ +
+ + + + + + diff --git a/tests/data/expected_whatcom_table.txt b/tests/data/expected_whatcom_table.txt new file mode 100644 index 00000000..3b61d5f8 --- /dev/null +++ b/tests/data/expected_whatcom_table.txt @@ -0,0 +1,6 @@ ++----+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ +| | System Size | Setback Requirement | +|----+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| 0 | SWES | 1 times total height of SWES structure | +| 1 | WES | 1,000 feet from a property line of any property in other than the HII district. If the neighboring property is in an HII district the setback is 1 times the total height.1 | ++----+---------------+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ \ No newline at end of file diff --git a/tests/data/indiana_general_ord.pdf b/tests/data/indiana_general_ord.pdf new file mode 100644 index 00000000..c4db9d99 Binary files /dev/null and b/tests/data/indiana_general_ord.pdf differ diff --git a/tests/data/tc.pdf b/tests/data/tc.pdf new file mode 100644 index 00000000..72e81a2f Binary files /dev/null and b/tests/data/tc.pdf differ diff --git a/tests/ords/services/test_services_base.py b/tests/ords/services/test_services_base.py new file mode 100644 index 00000000..2201b681 --- /dev/null +++ b/tests/ords/services/test_services_base.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-argument +"""Test ELM Ordinances Base Services""" +import time +from pathlib import Path + +import pytest + +from elm.ords.services.base import RateLimitedService +from elm.ords.services.usage import TimeBoundedUsageTracker + + +def test_rate_limited_service(): + """Test base implementation of `RateLimitedService` class""" + + class TestService(RateLimitedService): + """Simple service implementation for tests.""" + + async def process(self, *args, **kwargs): + """Always return 0.""" + return 0 + + rate_tracker = TimeBoundedUsageTracker(max_seconds=5) + service = TestService(rate_limit=100, rate_tracker=rate_tracker) + + assert service.can_process + service.rate_tracker.add(50) + assert service.can_process + service.rate_tracker.add(75) + assert not service.can_process + time.sleep(6) + assert service.can_process + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/services/test_services_openai.py b/tests/ords/services/test_services_openai.py new file mode 100644 index 00000000..d4c2f5a8 --- /dev/null +++ b/tests/ords/services/test_services_openai.py @@ -0,0 +1,119 @@ +# -*- coding: utf-8 -*- +# pylint: disable=unused-argument +"""Test ELM Ordinance openai services""" +from pathlib import Path + +import httpx +import pytest +import openai + +from elm.ords.services.openai import ( + count_tokens, + usage_from_response, + OpenAIService, +) +from elm.ords.services.usage import UsageTracker + + +TEST_MESSAGES_1 = [ + {"role": "system", "content": "You are a friendly bot"}, + {"role": "user", "content": "How are you?"}, +] +TEST_MESSAGES_2 = [ + {"role": "system", "content": "You are a friendly bot"}, + {"role": "user", "content": "I have 5 apples."}, + {"role": "system", "content": "Great!"}, + {"role": "user", "content": "How many apples do you have?."}, +] + + +@pytest.mark.parametrize( + "messages, model, token_count", + [(TEST_MESSAGES_1, "gpt-4", 20), (TEST_MESSAGES_2, "gpt-4", 39)], +) +def test_count_tokens(messages, model, token_count): + """Test `count_tokens` function""" + assert count_tokens(messages, model) == token_count + + +@pytest.mark.parametrize( + "usage_input, expected_output", + [ + ({}, {"requests": 1, "prompt_tokens": 100, "response_tokens": 10}), + ( + {"requests": 10, "response_tokens": 100}, + {"requests": 11, "prompt_tokens": 100, "response_tokens": 110}, + ), + ], +) +def test_usage_from_response( + usage_input, expected_output, sample_openai_response +): + """Test `usage_from_response` function""" + response = sample_openai_response() + assert usage_from_response(usage_input, response) == expected_output + + +@pytest.mark.asyncio +async def test_openai_service(sample_openai_response, monkeypatch): + """Test querying OpenAI while tracking limits and usage""" + + async def _test_response(*args, **kwargs): + if kwargs.get("bad_request"): + response = httpx.Response(404) + response.request = httpx.Request(method="test", url="test") + raise openai.BadRequestError( + "for testing", response=response, body=None + ) + return sample_openai_response(kwargs=kwargs) + + client = openai.AsyncOpenAI() + monkeypatch.setattr( + client.chat.completions, + "create", + _test_response, + raising=True, + ) + openai_service = OpenAIService(client) + + usage_tracker = UsageTracker("my_county", usage_from_response) + + message = await openai_service.process( + usage_tracker=usage_tracker, model="gpt-4" + ) + assert openai_service.rate_tracker.total == 13 + assert message == "test_response" + + assert usage_tracker == { + "default": { + "requests": 1, + "prompt_tokens": 100, + "response_tokens": 10, + } + } + + message = await openai_service.process( + usage_tracker=usage_tracker, model="gpt-4", bad_request=True + ) + assert message is None + assert openai_service.rate_tracker.total == 16 + assert usage_tracker == { + "default": { + "requests": 1, + "prompt_tokens": 100, + "response_tokens": 10, + } + } + + await openai_service.process(model="gpt-4") + assert usage_tracker == { + "default": { + "requests": 1, + "prompt_tokens": 100, + "response_tokens": 10, + } + } + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/services/test_services_provider.py b/tests/ords/services/test_services_provider.py new file mode 100644 index 00000000..4871a5fa --- /dev/null +++ b/tests/ords/services/test_services_provider.py @@ -0,0 +1,288 @@ +# -*- coding: utf-8 -*- +# pylint: disable=missing-class-docstring,missing-function-docstring +# pylint: disable=unused-argument +"""Test Service Provider""" +import asyncio +from pathlib import Path + +import pytest + +from elm.ords.services.base import Service +from elm.ords.services.provider import RunningAsyncServices, _RunningProvider +from elm.ords.utilities.exceptions import ( + ELMOrdsNotInitializedError, + ELMOrdsValueError, +) + + +@pytest.mark.asyncio +async def test_services_provider(service_base_class): + """Test that services provider works as expected""" + + job_order, TestService = service_base_class + + class AlwaysThreeService(TestService): + NUMBER = 3 + LEN_SLEEP = 5 + + class AlwaysTenService(TestService): + NUMBER = 5 + + with pytest.raises(ELMOrdsNotInitializedError): + AlwaysTenService._queue() + + with pytest.raises(ELMOrdsValueError): + async with RunningAsyncServices([]): + pass + + services = [AlwaysThreeService(), AlwaysTenService()] + async with RunningAsyncServices(services): + a3_producers = [ + asyncio.create_task(AlwaysThreeService.call(i)) for i in range(4) + ] + a10_producers = [ + asyncio.create_task(AlwaysTenService.call(i)) for i in range(7) + ] + out = await asyncio.gather(*(a3_producers + a10_producers)) + + expected_job_out = [3, 3, 3, 3, 5, 5, 5, 5, 5, 5, 5] + expected_job_order = [ + (3, 0), + (5, 0), + (3, 1), + (5, 1), + (3, 2), + (5, 2), + (5, 3), + (5, 4), + (5, 5), + (5, 6), + (3, 3), + ] + assert out == expected_job_out, f"{out=}" + assert job_order == expected_job_order, f"{job_order=}" + + +@pytest.mark.asyncio +async def test_services_provider_staggered_jobs(service_base_class): + """Test that services provider works as expected with staggered jobs""" + + job_order, TestService = service_base_class + + class AlwaysThreeService(TestService): + NUMBER = 3 + LEN_SLEEP = 5 + STAGGER = 1 + + class AlwaysTenService(TestService): + NUMBER = 5 + LEN_SLEEP = 8 + STAGGER = 1 + + services = [AlwaysThreeService(), AlwaysTenService()] + async with RunningAsyncServices(services): + a3_producers = [ + asyncio.create_task(AlwaysThreeService.call(i)) for i in range(5) + ] + a10_producers = [ + asyncio.create_task(AlwaysTenService.call(i)) for i in range(8) + ] + out = await asyncio.gather(*(a3_producers + a10_producers)) + + expected_job_out = [3, 3, 3, 3, 3, 5, 5, 5, 5, 5, 5, 5, 5] + expected_job_order = [ + (3, 0), + (5, 0), + (3, 1), + (5, 1), + (3, 2), + (5, 2), + (5, 3), + (5, 4), + (3, 3), + (3, 4), + (5, 5), + (5, 6), + (5, 7), + ] + assert out == expected_job_out, f"{out=}" + assert job_order == expected_job_order, f"{job_order=}" + + +@pytest.mark.asyncio +async def test_services_provider_no_submissions_allowed_at_start( + service_base_class, +): + """Test that services provider works even when service is not ready.""" + + job_order, TestService = service_base_class + + class AlwaysThreeService(TestService): + NUMBER = 3 + + def __init__(self): + super().__init__() + self.n_requests = -1 + + @property + def can_process(self): + self.n_requests += 1 + if self.n_requests < 10: + return False + return super().can_process + + services = [AlwaysThreeService()] + async with RunningAsyncServices(services): + a3_producers = [ + asyncio.create_task(AlwaysThreeService.call(i)) for i in range(4) + ] + out = await asyncio.gather(*a3_producers) + + expected_job_out = [3, 3, 3, 3] + expected_job_order = [(3, 0), (3, 1), (3, 2), (3, 3)] + assert out == expected_job_out, f"{out=}" + assert job_order == expected_job_order, f"{job_order=}" + + +@pytest.mark.asyncio +async def test_services_provider_raises_error(): + """Test that services provider raises error if service does.""" + + class BadService(Service): + @property + def can_process(self): + return True + + async def process(self, *args, **kwargs): + raise ValueError("A test error") + + services = [BadService()] + with pytest.raises(ValueError) as exc_info: + async with RunningAsyncServices(services): + await BadService.call() + + assert "A test error" in str(exc_info) + + +@pytest.mark.asyncio +async def test_services_provider_submits_as_long_as_needed(monkeypatch): + """Test that services provider continues to submit jobs while it can.""" + + call_cache = [] + + async def collect_responses(self): + call_cache.append(len(self.jobs)) + if not self.jobs: + return + complete, __ = await asyncio.wait( + self.jobs, return_when=asyncio.FIRST_COMPLETED + ) + for job in complete: + self.jobs.remove(job) + + monkeypatch.setattr( + _RunningProvider, + "collect_responses", + collect_responses, + raising=True, + ) + + class FastService(Service): + @property + def can_process(self): + return True + + async def process(self, *args, **kwargs): + return True + + services = [FastService()] + async with RunningAsyncServices(services): + producers = [ + asyncio.create_task(FastService.call()) for _ in range(10) + ] + out = await asyncio.gather(*producers) + + assert out == [True] * 10 + assert not call_cache + + +@pytest.mark.asyncio +async def test_services_provider_not_exceed_max_jobs(monkeypatch): + """Test that services provider doesn't exceed max concurrent job count.""" + + call_cache = [] + + async def collect_responses(self): + call_cache.append(len(self.jobs)) + if not self.jobs: + return + complete, __ = await asyncio.wait( + self.jobs, return_when=asyncio.FIRST_COMPLETED + ) + for job in complete: + self.jobs.remove(job) + + monkeypatch.setattr( + _RunningProvider, + "collect_responses", + collect_responses, + raising=True, + ) + + class LimitedFastService(Service): + MAX_CONCURRENT_JOBS = 5 + + @property + def can_process(self): + return True + + async def process(self, *args, **kwargs): + return True + + services = [LimitedFastService()] + async with RunningAsyncServices(services): + producers = [ + asyncio.create_task(LimitedFastService.call()) for _ in range(10) + ] + out = await asyncio.gather(*producers) + + assert out == [True] * 10 + assert call_cache == [5, 5] + + +@pytest.mark.asyncio +async def test_services_provider_acquire_and_release_service_resources(): + """Test that services provider doesn't exceed max concurrent job count.""" + + call_cache = [] + + class FastResourceService(Service): + + @property + def can_process(self): + return True + + async def process(self, *args, **kwargs): + return True + + def acquire_resources(self): + call_cache.append("acquired") + + def release_resources(self): + call_cache.append("released") + + services = [FastResourceService()] + assert not call_cache + async with RunningAsyncServices(services): + assert call_cache == ["acquired"] + producers = [ + asyncio.create_task(FastResourceService.call()) for _ in range(10) + ] + out = await asyncio.gather(*producers) + + assert out == [True] * 10 + assert call_cache == ["acquired", "released"] + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/services/test_services_queues.py b/tests/ords/services/test_services_queues.py new file mode 100644 index 00000000..f496c857 --- /dev/null +++ b/tests/ords/services/test_services_queues.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +"""Test Service Queues""" +import asyncio +from pathlib import Path + +import pytest + +from elm.ords.services.queues import ( + initialize_service_queue, + tear_down_service_queue, + get_service_queue, +) + + +@pytest.fixture +def service_name(): + """Create a service name that definitely has no queue""" + name = "test" + tear_down_service_queue(name) + yield name + tear_down_service_queue(name) + + +def test_initialize_service_queue(service_name): + """Test initializing a queue""" + + queue = initialize_service_queue(service_name) + assert isinstance(queue, asyncio.Queue) + + queue2 = initialize_service_queue(service_name) + assert queue is queue2 + + +def test_tear_down_service_queue(service_name): + """Test tearing down a queue""" + + tear_down_service_queue(service_name) + assert get_service_queue(service_name) is None + + initialize_service_queue(service_name) + queue = get_service_queue(service_name) + assert isinstance(queue, asyncio.Queue) + + queue2 = get_service_queue(service_name) + assert queue is queue2 + + tear_down_service_queue(service_name) + assert get_service_queue(service_name) is None + + +def test_get_service_queue(): + """Test retrieving a queue""" + + assert get_service_queue(service_name) is None + + initialize_service_queue(service_name) + queue = get_service_queue(service_name) + assert isinstance(queue, asyncio.Queue) + + tear_down_service_queue(service_name) + assert get_service_queue(service_name) is None + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/services/test_services_threaded.py b/tests/ords/services/test_services_threaded.py new file mode 100644 index 00000000..4f6da6a7 --- /dev/null +++ b/tests/ords/services/test_services_threaded.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinances TempFileCache Services""" +from pathlib import Path + +import pytest + +from elm.web.document import HTMLDocument +from elm.ords.services.threaded import TempFileCache, FileMover + + +@pytest.mark.asyncio +async def test_temp_file_cache_service(): + """Test base implementation of `TempFileCache` class""" + + doc = HTMLDocument(["test"]) + doc.metadata["source"] = "http://www.example.com/?=%20test" + + cache = TempFileCache() + cache.acquire_resources() + out_fp = await cache.process(doc, doc.text) + assert out_fp.exists() + assert out_fp.read_text().startswith("test") + cache.release_resources() + assert not out_fp.exists() + + +@pytest.mark.asyncio +async def test_file_move_service(tmp_path): + """Test base implementation of `FileMover` class""" + + doc = HTMLDocument(["test"]) + doc.metadata["source"] = "http://www.example.com/?=%20test" + + cache = TempFileCache() + cache.acquire_resources() + out_fp = await cache.process(doc, doc.text) + assert out_fp.exists() + assert out_fp.read_text().startswith("test") + doc.metadata["cache_fn"] = out_fp + + expected_moved_fp = tmp_path / out_fp.name + assert not expected_moved_fp.exists() + mover = FileMover(tmp_path) + mover.acquire_resources() + moved_fp = await mover.process(doc) + assert expected_moved_fp == moved_fp + assert not out_fp.exists() + assert moved_fp.exists() + assert moved_fp.read_text().startswith("test") + + cache.release_resources() + mover.release_resources() + assert moved_fp.exists() + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/services/test_services_usage.py b/tests/ords/services/test_services_usage.py new file mode 100644 index 00000000..4ed2d7c7 --- /dev/null +++ b/tests/ords/services/test_services_usage.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance service usage functions and classes""" +import time +from pathlib import Path + +import pytest + +from elm.ords.services.usage import ( + TimedEntry, + TimeBoundedUsageTracker, + UsageTracker, +) + + +def _sample_response_parser(current_usage, response): + """Sample response to usage conversion function""" + current_usage["requests"] = current_usage.get("requests", 0) + 1 + if "tokens" in response: + current_usage["tokens"] = response["tokens"] + inputs = current_usage.get("inputs", 0) + current_usage["inputs"] = inputs + response.get("inputs", 0) + return current_usage + + +def test_timed_entry(): + """Test `TimedEntry` class""" + + a = TimedEntry(100) + assert a <= time.monotonic() + + time.sleep(1) + sample_time = time.monotonic() + time.sleep(1) + b = TimedEntry(10000) + assert b > sample_time + assert a < sample_time + + assert a.value == 100 + assert b.value == 10000 + + +def test_time_bounded_usage_tracker(): + """Test the `TimeBoundedUsageTracker` class""" + + tracker = TimeBoundedUsageTracker(max_seconds=5) + assert tracker.total == 0 + tracker.add(500) + assert tracker.total == 500 + time.sleep(3) + tracker.add(200) + assert tracker.total == 700 + time.sleep(3) + assert tracker.total == 200 + time.sleep(3) + assert tracker.total == 0 + + +def test_usage_tracker(): + """Test the `UsageTracker` class""" + + tracker = UsageTracker("test", response_parser=_sample_response_parser) + assert tracker == {} + assert tracker.totals == {} + + tracker.update_from_model() + assert tracker == {} + assert tracker.totals == {} + + tracker.update_from_model({}) + assert tracker == {"default": {"requests": 1, "inputs": 0}} + assert tracker.totals == {"requests": 1, "inputs": 0} + + tracker.update_from_model({"inputs": 100}, sub_label="parsing") + tracker.update_from_model() + + assert tracker == { + "default": {"requests": 1, "inputs": 0}, + "parsing": {"requests": 1, "inputs": 100}, + } + assert tracker.totals == {"requests": 2, "inputs": 100} + + tracker.update_from_model({"tokens": 5}) + + assert tracker == { + "default": {"requests": 2, "inputs": 0, "tokens": 5}, + "parsing": {"requests": 1, "inputs": 100}, + } + assert tracker.totals == {"requests": 3, "inputs": 100, "tokens": 5} + + output = {"some": "value"} + tracker.add_to(output) + expected_out = {**tracker, "tracker_totals": tracker.totals} + assert output == {"some": "value", "test": expected_out} + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/test_integrated.py b/tests/ords/test_integrated.py new file mode 100644 index 00000000..98803b1f --- /dev/null +++ b/tests/ords/test_integrated.py @@ -0,0 +1,231 @@ +# -*- coding: utf-8 -*- +# pylint: disable=missing-class-docstring,missing-function-docstring +# pylint: disable=unused-argument +"""ELM Ordinance integration tests""" +import time +import logging +import asyncio +from pathlib import Path +from contextlib import asynccontextmanager + +import aiohttp +import httpx +import pytest +import openai + +import elm.web.html_pw +from elm import TEST_DATA_DIR +from elm.ords.services.usage import TimeBoundedUsageTracker, UsageTracker +from elm.ords.services.openai import OpenAIService, usage_from_response +from elm.ords.services.threaded import TempFileCache +from elm.ords.services.provider import RunningAsyncServices +from elm.ords.utilities.queued_logging import LocationFileLog, LogListener +from elm.web.google_search import PlaywrightGoogleLinkSearch +from elm.web.file_loader import AsyncFileLoader +from elm.web.document import HTMLDocument + + +WHATCOM_DOC_PATH = Path(TEST_DATA_DIR) / "Whatcom.txt" + + +class MockResponse: + def __init__(self, read_return): + self.read_return = read_return + + async def read(self): + return self.read_return + + +@asynccontextmanager +async def patched_get(session, url, *args, **kwargs): + if url == "Whatcom": + with open(WHATCOM_DOC_PATH, "rb") as fh: + content = fh.read() + + yield MockResponse(content) + + +async def patched_get_html(url, *args, **kwargs): + with open(WHATCOM_DOC_PATH, "r", encoding="utf-8") as fh: + content = fh.read() + + return content + + +@pytest.mark.asyncio +async def test_openai_query(sample_openai_response, monkeypatch): + """Test querying OpenAI while tracking limits and usage""" + + start_time = None + elapsed_times = [] + + async def _test_response(*args, **kwargs): + time_elapsed = time.monotonic() - start_time + elapsed_times.append(time_elapsed) + if time_elapsed < 3: + response = httpx.Response(404) + response.request = httpx.Request(method="test", url="test") + raise openai.RateLimitError( + "for testing", response=response, body=None + ) + + if kwargs.get("bad_request"): + response = httpx.Response(404) + response.request = httpx.Request(method="test", url="test") + raise openai.BadRequestError( + "for testing", response=response, body=None + ) + return sample_openai_response() + + client = openai.AsyncOpenAI() + monkeypatch.setattr( + client.chat.completions, + "create", + _test_response, + raising=True, + ) + rate_tracker = TimeBoundedUsageTracker(max_seconds=5) + openai_service = OpenAIService( + client, rate_limit=3, rate_tracker=rate_tracker + ) + + usage_tracker = UsageTracker("my_county", usage_from_response) + async with RunningAsyncServices([openai_service]): + start_time = time.monotonic() + message = await OpenAIService.call( + usage_tracker=usage_tracker, model="gpt-4" + ) + message2 = await OpenAIService.call(model="gpt-4") + + assert openai_service.rate_tracker.total == 13 + assert message == "test_response" + assert message2 == "test_response" + assert len(elapsed_times) == 3 + assert elapsed_times[0] < 1 + assert elapsed_times[1] >= 4 + assert elapsed_times[2] >= 9 + + assert usage_tracker == { + "default": { + "requests": 1, + "prompt_tokens": 100, + "response_tokens": 10, + } + } + + time.sleep(6) + assert openai_service.rate_tracker.total == 0 + + start_time = time.monotonic() - 4 + await OpenAIService.call(model="gpt-4") + await OpenAIService.call(model="gpt-4") + assert len(elapsed_times) == 5 + assert elapsed_times[-2] - 4 < 1 + assert elapsed_times[-1] - 4 > 5 + + time.sleep(6) + start_time = time.monotonic() - 4 + assert openai_service.rate_tracker.total == 0 + message = await OpenAIService.call( + usage_tracker=usage_tracker, model="gpt-4", bad_request=True + ) + assert message is None + assert openai_service.rate_tracker.total <= 3 + assert usage_tracker == { + "default": { + "requests": 1, + "prompt_tokens": 100, + "response_tokens": 10, + } + } + + +@pytest.mark.asyncio +async def test_google_search_with_logging(tmp_path): + """Test searching google for some counties with logging""" + + assert not list(tmp_path.glob("*")) + + logger = logging.getLogger("search_test") + test_locations = ["El Paso County, Colorado", "Decatur County, Indiana"] + num_requested_links = 10 + + async def search_single(location): + logger.info("This location is %r", location) + search_engine = PlaywrightGoogleLinkSearch() + return await search_engine.results( + f"Wind energy zoning ordinance {location}", + num_results=num_requested_links, + ) + + async def search_location_with_logs( + listener, log_dir, location, level="INFO" + ): + with LocationFileLog( + listener, log_dir, location=location, level=level + ): + logger.info("A generic test log") + return await search_single(location) + + log_dir = tmp_path / "logs" + log_listener = LogListener(["search_test"], level="DEBUG") + async with log_listener as ll: + searchers = [ + asyncio.create_task( + search_location_with_logs(ll, log_dir, loc, level="DEBUG"), + name=loc, + ) + for loc in test_locations + ] + output = await asyncio.gather(*searchers) + + expected_words = ["paso", "decatur"] + assert len(output) == 2 + for query_results, expected_word in zip(output, expected_words): + assert len(query_results) == 1 + assert len(query_results[0]) == num_requested_links + assert any(expected_word in link for link in query_results[0]) + + log_files = list(log_dir.glob("*")) + assert len(log_files) == 2 + for fp in log_files: + assert ( + fp.read_text() + == f"A generic test log\nThis location is {fp.stem!r}\n" + ) + + +@pytest.mark.asyncio +async def test_async_file_loader_with_temp_cache(monkeypatch): + """Test `AsyncFileLoader` with a `TempFileCache` service""" + + monkeypatch.setattr( + aiohttp.ClientSession, + "get", + patched_get, + raising=True, + ) + monkeypatch.setattr( + elm.web.html_pw, + "_load_html", + patched_get_html, + raising=True, + ) + + with open(WHATCOM_DOC_PATH, "r", encoding="utf-8") as fh: + content = fh.read() + + truth = HTMLDocument([content]) + + async with RunningAsyncServices([TempFileCache()]): + loader = AsyncFileLoader(file_cache_coroutine=TempFileCache.call) + doc = await loader.fetch(url="Whatcom") + assert doc.text == truth.text + assert doc.metadata["source"] == "Whatcom" + cached_fp = doc.metadata["cache_fn"] + assert cached_fp.exists() + assert cached_fp.read_text(encoding="utf-8") == doc.text + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/utilities/test_utilities_counties.py b/tests/ords/utilities/test_utilities_counties.py new file mode 100644 index 00000000..c9f2d9e0 --- /dev/null +++ b/tests/ords/utilities/test_utilities_counties.py @@ -0,0 +1,90 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance county utilities tests. """ +from pathlib import Path + +import pytest +import pandas as pd + +from elm.ords.utilities.counties import ( + load_all_county_info, + load_counties_from_fp, + county_websites, +) +from elm.ords.utilities.exceptions import ELMOrdsValueError + + +def test_load_counties(): + """Test the `load_all_county_info` function.""" + + county_info = load_all_county_info() + assert not county_info.empty + + expected_cols = [ + "County", + "State", + "FIPS", + "County Type", + "Full Name", + "Website", + ] + assert all(col in county_info for col in expected_cols) + assert len(county_info) == len(county_info.groupby(["County", "State"])) + + # Spot checks: + assert "Decatur" in set(county_info["County"]) + assert "Box Elder" in set(county_info["County"]) + assert "Colorado" in set(county_info["State"]) + assert "Rhode Island" in set(county_info["State"]) + + +def test_county_websites(): + """Test the `county_websites` function""" + + websites = county_websites() + assert len(websites) == len(load_all_county_info()) + assert isinstance(websites, dict) + assert all(isinstance(key, tuple) for key in websites) + assert all(len(key) == 2 for key in websites) + + # Spot checks: + assert ("decatur", "indiana") in websites + assert ("el paso", "colorado") in websites + assert ("box elder", "utah") in websites + + +def test_load_counties_from_fp(tmp_path): + """Test `load_counties_from_fp` function.""" + + test_county_fp = tmp_path / "out.csv" + input_counties = pd.DataFrame( + {"County": ["decatur", "DNE County"], "State": ["INDIANA", "colorado"]} + ) + input_counties.to_csv(test_county_fp) + + counties = load_counties_from_fp(test_county_fp) + + assert len(counties) == 1 + assert set(counties["County"]) == {"Decatur"} + assert set(counties["State"]) == {"Indiana"} + assert {type(val) for val in counties["FIPS"]} == {int} + + +def test_load_counties_from_fp_bad_input(tmp_path): + """Test `load_counties_from_fp` function.""" + + test_county_fp = tmp_path / "out.csv" + pd.DataFrame().to_csv(test_county_fp) + + with pytest.raises(ELMOrdsValueError) as err: + load_counties_from_fp(test_county_fp) + + expected_msg = ( + "The following required columns were not found in the county input:" + ) + assert expected_msg in str(err) + assert "County" in str(err) + assert "State" in str(err) + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/utilities/test_utilities_exceptions.py b/tests/ords/utilities/test_utilities_exceptions.py new file mode 100644 index 00000000..ded4e385 --- /dev/null +++ b/tests/ords/utilities/test_utilities_exceptions.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance exception types. + +Most exception logic + tests pulled from GAPs +(https://github.com/NREL/gaps) +""" +from pathlib import Path + +import pytest + +from elm.ords.utilities.exceptions import ( + ELMOrdsError, + ELMOrdsValueError, + ELMOrdsRuntimeError, + ELMOrdsNotInitializedError, +) + + +BASIC_ERROR_MESSAGE = "An error message" + + +def test_exceptions_log_error(caplog, assert_message_was_logged): + """Test that a raised exception logs message, if any.""" + + try: + raise ELMOrdsError + except ELMOrdsError: + pass + + assert not caplog.records + + try: + raise ELMOrdsError(BASIC_ERROR_MESSAGE) + except ELMOrdsError: + pass + + assert_message_was_logged(BASIC_ERROR_MESSAGE, "ERROR") + + +def test_exceptions_log_uncaught_error(assert_message_was_logged): + """Test that a raised exception logs message if uncaught.""" + + with pytest.raises(ELMOrdsError): + raise ELMOrdsError(BASIC_ERROR_MESSAGE) + + assert_message_was_logged(BASIC_ERROR_MESSAGE, "ERROR") + + +@pytest.mark.parametrize( + "raise_type, catch_types", + [ + ( + ELMOrdsNotInitializedError, + [ELMOrdsError, ELMOrdsNotInitializedError], + ), + ( + ELMOrdsValueError, + [ELMOrdsError, ValueError, ELMOrdsValueError], + ), + ( + ELMOrdsRuntimeError, + [ELMOrdsError, RuntimeError, ELMOrdsRuntimeError], + ), + ], +) +def test_catching_error_by_type( + raise_type, catch_types, assert_message_was_logged +): + """Test that gaps exceptions are caught correctly.""" + for catch_type in catch_types: + with pytest.raises(catch_type) as exc_info: + raise raise_type(BASIC_ERROR_MESSAGE) + + assert BASIC_ERROR_MESSAGE in str(exc_info.value) + assert_message_was_logged(BASIC_ERROR_MESSAGE, "ERROR") + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/utilities/test_utilities_location.py b/tests/ords/utilities/test_utilities_location.py new file mode 100644 index 00000000..93af8918 --- /dev/null +++ b/tests/ords/utilities/test_utilities_location.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +"""ELM Ordinance Location utility tests. """ +from pathlib import Path + +import pytest + +from elm.ords.utilities.location import County + + +def test_basic_county_properties(): + """Tets basic properties for ``County`` class""" + + county = County("Box Elder", "Utah") + + assert repr(county) == "County(Box Elder, Utah, is_parish=False)" + assert county.full_name == "Box Elder County, Utah" + assert county.full_name == str(county) + + assert county == County("Box elder", "uTah") + assert county != County("Box Elder", "Utah", is_parish=True) + + assert county == "Box Elder County, Utah" + assert county == "Box elder, Utah" + + +def test_basic_parish_properties(): + """Tets basic properties for ``County`` class with ``is_parish=True``""" + + county = County("Box Elder", "Utah", is_parish=True) + + assert repr(county) == "County(Box Elder, Utah, is_parish=True)" + assert county.full_name == "Box Elder Parish, Utah" + assert county.full_name == str(county) + + assert county == County("Box elder", "uTah", is_parish=True) + assert county != County("Box Elder", "Utah", is_parish=False) + + assert county == "Box Elder Parish, Utah" + assert county == "Box elder, Utah" + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/utilities/test_utilities_parsing.py b/tests/ords/utilities/test_utilities_parsing.py new file mode 100644 index 00000000..59d11428 --- /dev/null +++ b/tests/ords/utilities/test_utilities_parsing.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance parsing utilities""" +from pathlib import Path + +import pytest + +from elm.ords.utilities.parsing import ( + llm_response_as_json, + merge_overlapping_texts, +) + + +@pytest.mark.parametrize( + "in_str,expected", + [ + (' {"a": 1} ', {"a": 1}), + ('```json\n{"a": True, "b": False}```', {"a": True, "b": False}), + ('{"a": True', {}), + ], +) +def test_sync_retry(in_str, expected): + """Test the `llm_response_as_json` function""" + + assert llm_response_as_json(in_str) == expected + + +@pytest.mark.parametrize( + "text_chunks,n,expected", + [ + ( + [ + "Some text. Some overlap. More text. More text that " + "shouldn't be touched. Some overlap.", + "Some overlap. More text.", + "Some non-overlapping text.", + ], + 12, + "Some text. Some overlap. More text. More text that " + "shouldn't be touched. Some overlap. More text.\nSome " + "non-overlapping text.", + ) + ], +) +def test_merge_overlapping_texts(text_chunks, n, expected): + """Test the `merge_overlapping_texts` function""" + + assert merge_overlapping_texts(text_chunks, n) == expected + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/utilities/test_utilities_queued_logging.py b/tests/ords/utilities/test_utilities_queued_logging.py new file mode 100644 index 00000000..2f42b5b6 --- /dev/null +++ b/tests/ords/utilities/test_utilities_queued_logging.py @@ -0,0 +1,65 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance logging logic. """ +import logging +import asyncio +from pathlib import Path + +import pytest + +from elm.ords.services.provider import RunningAsyncServices +from elm.ords.utilities.queued_logging import LocationFileLog, LogListener + + +@pytest.mark.asyncio +async def test_logs_sent_to_separate_files(tmp_path, service_base_class): + """Test that logs are correctly sent to individual files.""" + + logger = logging.getLogger("ords") + test_locations = ["a", "bc", "def", "ghij"] + __, TestService = service_base_class + + assert not logger.handlers + + class AlwaysThreeService(TestService): + """Test service that returns ``3``.""" + + NUMBER = 3 + LEN_SLEEP = 5 + + async def process_single(val): + """Call `AlwaysThreeService`.""" + logger.info(f"This location is {val!r}") + return await AlwaysThreeService.call(len(val)) + + async def process_location_with_logs(listener, log_dir, location): + """Process location and record logs for tests.""" + with LocationFileLog(listener, log_dir, location=location): + logger.info("A generic test log") + return await process_single(location) + + log_dir = tmp_path / "ord_logs" + services = [AlwaysThreeService()] + loggers = ["ords"] + + async with RunningAsyncServices(services), LogListener(loggers) as ll: + producers = [ + asyncio.create_task( + process_location_with_logs(ll, log_dir, loc), name=loc + ) + for loc in test_locations + ] + await asyncio.gather(*producers) + + assert not logger.handlers + + log_files = list(log_dir.glob("*")) + assert len(log_files) == len(test_locations) + for loc in test_locations: + expected_log_file = log_dir / f"{loc}.log" + assert expected_log_file.exists() + log_text = expected_log_file.read_text(encoding="utf-8") + assert log_text == f"A generic test log\nThis location is {loc!r}\n" + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/validation/test_validation_content.py b/tests/ords/validation/test_validation_content.py new file mode 100644 index 00000000..6282036e --- /dev/null +++ b/tests/ords/validation/test_validation_content.py @@ -0,0 +1,84 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance content validation tests. """ +from pathlib import Path + +import pytest + +from elm.ords.validation.content import ( + ValidationWithMemory, + possibly_mentions_wind, +) + + +@pytest.mark.asyncio +async def test_validation_with_mem(): + """Test the `ValidationWithMemory` class (basic execution)""" + + sys_messages = [] + test_prompt = "Looking for key {key!r}" + + class MockStructuredLLMCaller: + """Mock LLM caller for tests.""" + + async def call(self, sys_msg, content, *__, **___): + """Mock LLM call and record system message""" + sys_messages.append(sys_msg) + return {"test": True} if content == 0 else {} + + text_chunks = list(range(7)) + validator = ValidationWithMemory(MockStructuredLLMCaller(), text_chunks, 3) + + out = await validator.parse_from_ind(0, test_prompt, key="test") + assert out + assert sys_messages == ["Looking for key 'test'"] + assert validator.memory == [{"test": True}, {}, {}, {}, {}, {}, {}] + + out = await validator.parse_from_ind(2, test_prompt, key="test") + assert out + assert sys_messages == ["Looking for key 'test'"] * 3 + assert validator.memory == [ + {"test": True}, + {"test": False}, + {"test": False}, + {}, + {}, + {}, + {}, + ] + + out = await validator.parse_from_ind(6, test_prompt, key="test") + assert not out + assert sys_messages == ["Looking for key 'test'"] * 6 + assert validator.memory == [ + {"test": True}, + {"test": False}, + {"test": False}, + {}, + {"test": False}, + {"test": False}, + {"test": False}, + ] + + +@pytest.mark.parametrize( + "text,truth", + [ + ("Wind SETBACKS", True), + (" WECS SETBACKS", True), + ("Window SETBACKS", False), + ("SWECS SETBACKS", False), + ("(wind LWET)", True), + ("Wind SWECS", False), + ("Wind WES", False), + ("Wind WES\n", True), + ("wind turbines and wind towers", True), + ], +) +def test_possibly_mentions_wind(text, truth): + """Test for `possibly_mentions_wind` function (basic execution)""" + + assert possibly_mentions_wind(text) == truth + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/ords/validation/test_validation_location.py b/tests/ords/validation/test_validation_location.py new file mode 100644 index 00000000..81124960 --- /dev/null +++ b/tests/ords/validation/test_validation_location.py @@ -0,0 +1,286 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance location validation tests. """ +import os +from pathlib import Path +from functools import partial + +import pytest +import openai +from langchain.text_splitter import RecursiveCharacterTextSplitter + +from elm import TEST_DATA_DIR, ApiBase +from elm.web.document import PDFDocument, HTMLDocument +from elm.utilities.parse import read_pdf +from elm.ords.llm import StructuredLLMCaller +from elm.ords.services.openai import OpenAIService +from elm.ords.services.provider import RunningAsyncServices +from elm.ords.utilities import RTS_SEPARATORS +from elm.ords.validation.location import ( + CountyValidator, + CountyNameValidator, + CountyJurisdictionValidator, + URLValidator, + _validator_check_for_doc, +) + + +SHOULD_SKIP = os.getenv("AZURE_OPENAI_API_KEY") is None +TESTING_TEXT_SPLITTER = RecursiveCharacterTextSplitter( + RTS_SEPARATORS, + chunk_size=3000, + chunk_overlap=300, + length_function=partial(ApiBase.count_tokens, model="gpt-4"), +) + + +@pytest.fixture(scope="module") +def oai_async_azure_client(): + """OpenAi Azure client to use for tests""" + return openai.AsyncAzureOpenAI( + api_key=os.environ.get("AZURE_OPENAI_API_KEY"), + api_version=os.environ.get("AZURE_OPENAI_VERSION"), + azure_endpoint=os.environ.get("AZURE_OPENAI_ENDPOINT"), + ) + + +@pytest.fixture() +def structured_llm_caller(): + """StructuredLLMCaller instance for testing""" + return StructuredLLMCaller( + llm_service=OpenAIService, + model="gpt-4", + temperature=0, + seed=42, + timeout=30, + ) + + +@pytest.mark.skipif(SHOULD_SKIP, reason="requires Azure OpenAI key") +@pytest.mark.asyncio +@pytest.mark.parametrize( + "county,state,url,truth", + [ + ( + "El Paso", + "Indiana", + "https://programs.dsireusa.org/system/program/detail/4332/" + "madison-county-wind-energy-systems-ordinance", + False, + ), + ( + "Madison", + "Indiana", + "https://programs.dsireusa.org/system/program/detail/4332/" + "madison-county-wind-energy-systems-ordinance", + False, + ), + ( + "Madison", + "North Carolina", + "https://programs.dsireusa.org/system/program/detail/4332/" + "madison-county-wind-energy-systems-ordinance", + False, + ), + ( + "Decatur", + "Indiana", + "http://www.decaturcounty.in.gov/doc/area-plan-commission/updates/" + "zoning_ordinance_-_article_13_wind_energy_conversion_system_" + "(WECS).pdf", + True, + ), + ( + "Decatur", + "Colorado", + "http://www.decaturcounty.in.gov/doc/area-plan-commission/updates/" + "zoning_ordinance_-_article_13_wind_energy_conversion_system_" + "(WECS).pdf", + False, + ), + ( + "El Paso", + "Indiana", + "http://www.decaturcounty.in.gov/doc/area-plan-commission/updates/" + "zoning_ordinance_-_article_13_wind_energy_conversion_system_" + "(WECS).pdf", + False, + ), + ], +) +async def test_url_matches_county( + oai_async_azure_client, structured_llm_caller, county, state, url, truth +): + """Test the URL validator class (basic execution)""" + url_validator = URLValidator(structured_llm_caller) + services = [OpenAIService(oai_async_azure_client, rate_limit=50_000)] + async with RunningAsyncServices(services): + out = await url_validator.check(url, county=county, state=state) + assert out == truth + + +@pytest.mark.skipif(SHOULD_SKIP, reason="requires Azure OpenAI key") +@pytest.mark.asyncio +@pytest.mark.parametrize( + "county,doc_fp,truth", + [ + ( + "Decatur", + Path(TEST_DATA_DIR) / "indiana_general_ord.pdf", + False, + ), + ( + "Decatur", + Path(TEST_DATA_DIR) / "Decatur Indiana.pdf", + True, + ), + ( + "Hamlin", + Path(TEST_DATA_DIR) / "Hamlin South Dakota.pdf", + True, + ), + ( + "Atlantic", + Path(TEST_DATA_DIR) / "Atlantic New Jersey.txt", + False, + ), + ( + "Barber", + Path(TEST_DATA_DIR) / "Barber Kansas.pdf", + False, + ), + ], +) +async def test_doc_matches_county_jurisdiction( + oai_async_azure_client, structured_llm_caller, county, doc_fp, truth +): + """Test the `CountyJurisdictionValidator` class (basic execution)""" + if doc_fp.suffix == ".pdf": + with open(doc_fp, "rb") as fh: + pages = read_pdf(fh.read()) + doc = PDFDocument(pages) + else: + with open(doc_fp, "r", encoding="utf-8") as fh: + text = fh.read() + doc = HTMLDocument([text], text_splitter=TESTING_TEXT_SPLITTER) + + cj_validator = CountyJurisdictionValidator(structured_llm_caller) + services = [OpenAIService(oai_async_azure_client, rate_limit=100_000)] + async with RunningAsyncServices(services): + out = await _validator_check_for_doc( + doc=doc, validator=cj_validator, county=county + ) + assert out == truth + + +@pytest.mark.skipif(SHOULD_SKIP, reason="requires Azure OpenAI key") +@pytest.mark.asyncio +@pytest.mark.parametrize( + "county,state,doc_fp,truth", + [ + ( + "Decatur", + "Indiana", + Path(TEST_DATA_DIR) / "Decatur Indiana.pdf", + True, + ), + ( + "Hamlin", + "South Dakota", + Path(TEST_DATA_DIR) / "Hamlin South Dakota.pdf", + True, + ), + ( + "Anoka", + "Minnesota", + Path(TEST_DATA_DIR) / "Anoka Minnesota.txt", + True, + ), + ], +) +async def test_doc_matches_county_name( + oai_async_azure_client, structured_llm_caller, county, state, doc_fp, truth +): + """Test the `CountyNameValidator` class (basic execution)""" + if doc_fp.suffix == ".pdf": + with open(doc_fp, "rb") as fh: + pages = read_pdf(fh.read()) + doc = PDFDocument(pages) + else: + with open(doc_fp, "r", encoding="utf-8") as fh: + text = fh.read() + doc = HTMLDocument([text], text_splitter=TESTING_TEXT_SPLITTER) + + cn_validator = CountyNameValidator(structured_llm_caller) + services = [OpenAIService(oai_async_azure_client, rate_limit=100_000)] + async with RunningAsyncServices(services): + out = await _validator_check_for_doc( + doc=doc, validator=cn_validator, county=county, state=state + ) + assert out == truth + + +@pytest.mark.skipif(SHOULD_SKIP, reason="requires Azure OpenAI key") +@pytest.mark.asyncio +@pytest.mark.parametrize( + "county,state,doc_fp,url,truth", + [ + ( + "Decatur", + "Indiana", + Path(TEST_DATA_DIR) / "Decatur Indiana.pdf", + "http://www.decaturcounty.in.gov/doc/area-plan-commission/z.pdf", + True, + ), + ( + "Hamlin", + "South Dakota", + Path(TEST_DATA_DIR) / "Hamlin South Dakota.pdf", + "http://www.test.gov", + True, + ), + ( + "Anoka", + "Minnesota", + Path(TEST_DATA_DIR) / "Anoka Minnesota.txt", + "http://www.test.gov", + False, + ), + ( + "Atlantic", + "New Jersey", + Path(TEST_DATA_DIR) / "Atlantic New Jersey.txt", + "http://www.test.gov", + False, + ), + ], +) +async def test_doc_matches_county( + oai_async_azure_client, + structured_llm_caller, + county, + state, + doc_fp, + url, + truth, +): + """Test the `CountyValidator` class (basic execution)""" + if doc_fp.suffix == ".pdf": + with open(doc_fp, "rb") as fh: + pages = read_pdf(fh.read()) + doc = PDFDocument(pages) + else: + with open(doc_fp, "r", encoding="utf-8") as fh: + text = fh.read() + doc = HTMLDocument([text], text_splitter=TESTING_TEXT_SPLITTER) + + doc.metadata["source"] = url + + county_validator = CountyValidator(structured_llm_caller) + services = [OpenAIService(oai_async_azure_client, rate_limit=100_000)] + async with RunningAsyncServices(services): + out = await county_validator.check(doc=doc, county=county, state=state) + assert out == truth + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/utilities/test_utilities_parse.py b/tests/utilities/test_utilities_parse.py new file mode 100644 index 00000000..3826c593 --- /dev/null +++ b/tests/utilities/test_utilities_parse.py @@ -0,0 +1,221 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance retry utilities""" +from pathlib import Path + +import pytest +import pdftotext + +from elm import TEST_DATA_DIR +from elm.utilities.parse import ( + clean_headers, + combine_pages, + is_multi_col, + format_html_tables, + html_to_text, + read_pdf, + remove_blank_pages, + replace_common_pdf_conversion_chars, + replace_excessive_newlines, + replace_multi_dot_lines, + remove_empty_lines_or_page_footers, +) + +SAMPLE_TABLE_TEXT = """Some text. + + + + + + + + + + + + + + + + +
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
+""" +EXPECTED_TABLE_OUT = """Some text. +| | Company | Contact | Country | +|---:|:---------------------------|:----------------|:----------| +| 0 | Alfreds Futterkiste | Maria Anders | Germany | +| 1 | Centro comercial Moctezuma | Francisco Chang | Mexico | +""" +PAGES_WITH_HEADERS_AND_FOOTERS = [ + "A title page", + "Page 1.\n---\nOnce upon a time in a digital realm\n....\npp.1", + "Page 2.\n---\nIn the vast expanse of ones and zeros\n....\npp.2", + "Page 3.\n---\nA narrative unfolded, threads of code\n....\npp.3", + "Page 4.\n---\nCharacters emerged, pixels on the screen\n....\npp.4", + "Page 5.\n---\nPlot twists encoded, through algorithms\n....\npp.5", + "Page 6.\n---\nWith each line, the story deepened\n....\npp.6", + "Page 7.\n---\nSyntax and semantics entwined, crafting a tale\n....\npp.7", + "Page 8.\n---\nIn the end, a digital landscape\n....\npp.8", + "Page 9.\n---\nleaving imprints in the memory bytes\n....\npp.9", + "", +] + + +def test_is_multi_col(): + """Test the `is_multi_col` heuristic function""" + + assert not is_multi_col("Some Text") + assert is_multi_col("Some Text") + assert is_multi_col( + """ + Some double here over + column text multiple lines. + given :) + """ + ) + assert not is_multi_col( + """ + Some text with odd spacing + and multiple lines but not + double column! + """ + ) + + +def test_remove_blank_pages(): + """Test the `remove_blank_pages` function""" + + assert remove_blank_pages([]) == [] + assert remove_blank_pages([""]) == [] + assert remove_blank_pages(["Here", ""]) == ["Here"] + assert remove_blank_pages(["We", " "]) == ["We"] + assert remove_blank_pages(["", " Go ", " ", "Again"]) == [" Go ", "Again"] + + +def test_format_html_tables(): + """Test the `format_html_tables` function (basic execution)""" + assert format_html_tables("test") == "test" + assert format_html_tables(SAMPLE_TABLE_TEXT) == EXPECTED_TABLE_OUT + + bad_table_text = SAMPLE_TABLE_TEXT + "\nBad table:\n
" + assert format_html_tables(bad_table_text) == bad_table_text + + +def test_clean_headers(): + """Test the `clean_headers` function (basic execution)""" + out = combine_pages(clean_headers(PAGES_WITH_HEADERS_AND_FOOTERS)) + + assert "A title page" in out + assert len(out) > 100 + assert "---" not in out + assert "...." not in out + assert "Page" not in out + assert "pp." not in out + + +def test_replace_common_pdf_conversion_chars(): + """Test the `replace_common_pdf_conversion_chars` function (basic exec.)""" + + out = replace_common_pdf_conversion_chars("Hello\r\n\x0cMy name is\r") + assert out == "Hello\nMy name is\n" + + +def test_replace_excessive_newlines(): + """Test the `replace_excessive_newlines` function (basic exec.)""" + + assert replace_excessive_newlines("\n") == "\n" + assert replace_excessive_newlines("\n\n") == "\n\n" + assert replace_excessive_newlines("\n\n\n") == "\n\n" + assert replace_excessive_newlines("\n\n\n\n") == "\n\n" + assert replace_excessive_newlines("\n\n\n \n \n\n\n") == "\n\n \n \n\n" + + +def test_replace_multi_dot_lines(): + """Test the `replace_multi_dot_lines` function (basic exec.)""" + + assert replace_multi_dot_lines(".") == "." + assert replace_multi_dot_lines("..") == ".." + assert replace_multi_dot_lines("...") == "..." + assert replace_multi_dot_lines("....") == "..." + assert replace_multi_dot_lines(".....") == "..." + assert replace_multi_dot_lines("......") == "..." + assert replace_multi_dot_lines("......\n......") == "...\n..." + + +def test_remove_empty_lines_or_page_footers(): + """Test the `remove_empty_lines_or_page_footers` function (basic exec.)""" + assert remove_empty_lines_or_page_footers("Hello\n 99\r!") == "Hello\n!" + assert remove_empty_lines_or_page_footers("Hello\n \r!") == "Hello\n!" + assert remove_empty_lines_or_page_footers("\n\r") == "\n" + + keep_str = "Hello\n Some text 99\r!" + assert remove_empty_lines_or_page_footers(keep_str) == keep_str + + multi_line_str = """ + 10. To regulate and restrict the erection, construction, reconstruction, + alteration, repair, and use of building, structures, and land. + + + + + 1 + CHAPTER 1.02 ORDINANCE PROVISIONS + """ + + expected_out = """ + 10. To regulate and restrict the erection, construction, reconstruction, + alteration, repair, and use of building, structures, and land. + CHAPTER 1.02 ORDINANCE PROVISIONS + """ + + assert remove_empty_lines_or_page_footers(multi_line_str) == expected_out + + +def test_html_to_text(): + """Test Document class for sample HTML file""" + doc_path = Path(TEST_DATA_DIR) / "Whatcom.txt" + + with open(doc_path, "r", encoding="utf-8") as fh: + og_text = fh.read() + + out = html_to_text(og_text) + + for tag in ["

", "", ""]: + assert tag in og_text + assert tag in out + + +@pytest.mark.parametrize( + "fn, physical", [("tc.pdf", False), ("GPT-4.pdf", True)] +) +def test_read_pdf(fn, physical): + """Test the `read_pdf` function (basic execution)""" + doc_path = Path(TEST_DATA_DIR) / fn + + with open(doc_path, "rb") as fh: + file_bytes = fh.read() + + pages = read_pdf(file_bytes) + + with open(doc_path, "rb") as fh: + truth = pdftotext.PDF(fh, physical=physical) + + assert all(t == p for t, p in zip(truth, pages)) + + +def test_read_pdf_bad_file(): + """Test the `read_pdf` function with bad file input""" + doc_path = Path(TEST_DATA_DIR) / "gpt4.txt" + + with open(doc_path, "rb") as fh: + file_bytes = fh.read() + + pages = read_pdf(file_bytes) + assert not pages + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/utilities/test_utilities_retry.py b/tests/utilities/test_utilities_retry.py new file mode 100644 index 00000000..90458d1b --- /dev/null +++ b/tests/utilities/test_utilities_retry.py @@ -0,0 +1,56 @@ +# -*- coding: utf-8 -*- +"""Test ELM Ordinance retry utilities""" +import time +import random +from pathlib import Path + +import pytest + +from elm.utilities.retry import ( + retry_with_exponential_backoff, + async_retry_with_exponential_backoff, +) +from elm.exceptions import ELMRuntimeError + + +@pytest.mark.parametrize("jitter, bounds", [(False, (2, 3)), (True, (4, 5))]) +def test_sync_retry(jitter, bounds, monkeypatch): + """Test the `retry_with_exponential_backoff` decorator""" + + monkeypatch.setattr(random, "random", lambda: 1, raising=True) + + @retry_with_exponential_backoff( + exponential_base=2, max_retries=1, jitter=jitter, errors=(ValueError,) + ) + def failing_function(): + raise ValueError("I'm broken") + + start_time = time.monotonic() + with pytest.raises(ELMRuntimeError): + failing_function() + elapsed_time = time.monotonic() - start_time + assert bounds[0] <= elapsed_time < bounds[1] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("jitter, bounds", [(False, (2, 3)), (True, (4, 5))]) +async def test_async_retry(jitter, bounds, monkeypatch): + """Test the `async_retry_with_exponential_backoff` decorator""" + + monkeypatch.setattr(random, "random", lambda: 1, raising=True) + + @async_retry_with_exponential_backoff( + exponential_base=2, max_retries=1, jitter=jitter, errors=(ValueError,) + ) + async def failing_function(): + raise ValueError("I'm broken") + + start_time = time.monotonic() + with pytest.raises(ELMRuntimeError): + await failing_function() + elapsed_time = time.monotonic() - start_time + assert bounds[0] <= elapsed_time < bounds[1] + + +if __name__ == "__main__": + pytest.main(["-q", "--show-capture=all", Path(__file__), "-rapP"]) diff --git a/tests/web/test_web_document.py b/tests/web/test_web_document.py new file mode 100644 index 00000000..5303bd07 --- /dev/null +++ b/tests/web/test_web_document.py @@ -0,0 +1,102 @@ +# -*- coding: utf-8 -*- +"""ELM Web Document class tests""" +from pathlib import Path + +import pytest +import pdftotext + +from elm import TEST_DATA_DIR +from elm.web.document import PDFDocument, HTMLDocument + + +class TestSplitter: + """Splitter class for testing purposes.""" + + def split_text(self, text): + """Split text on newlines.""" + return text.split("\n") + + +@pytest.mark.parametrize("doc_type", [PDFDocument, HTMLDocument]) +def test_basic_document(doc_type): + """Test basic properties of the `Document` class""" + + doc = doc_type([""]) + assert doc.text == "" + assert doc.raw_pages == [] + assert doc.metadata == {} + if doc_type is PDFDocument: + assert doc.num_raw_pages_to_keep == 0 + assert doc._last_page_index == 0 + assert doc.WRITE_KWARGS + assert doc.FILE_EXTENSION + + +def test_pdf_doc(): + """Test Document class for sample PDF file""" + doc_path = Path(TEST_DATA_DIR) / "GPT-4.pdf" + + with open(doc_path, "rb") as fh: + pdf = pdftotext.PDF(fh, physical=True) + + og_text = "\n".join(pdf) + doc = PDFDocument(pdf) + + assert 0 < len(doc.text) < len(og_text) + + assert "9/13/23, 11:23 AM" in og_text + assert "9/13/23, 11:23 AM" not in doc.text + + assert "\r\n" in og_text or "\n\n" in og_text + assert "\r\n" not in doc.text and "\n\n" not in doc.text + + assert doc.num_raw_pages_to_keep == 7 + assert doc._last_page_index == -2 + + # pylint: disable=unnecessary-comprehension + all_pages = [page for page in pdf] + expected_raw_pages = all_pages[:7] + all_pages[-2:] + assert doc.raw_pages == expected_raw_pages + + doc = PDFDocument(pdf, percent_raw_pages_to_keep=1000, max_raw_pages=1000) + assert doc.raw_pages == all_pages + + +def test_html_doc(): + """Test Document class for sample HTML file""" + doc_path = Path(TEST_DATA_DIR) / "Whatcom.txt" + + with open(doc_path, "r", encoding="utf-8") as fh: + og_text = fh.read() + + doc = HTMLDocument([og_text]) + + assert 0 < len(doc.text) < len(og_text) + for tag in ["

", "