Tools for processing shapefiles and looking up coordinates by address or parcel ID.
- Create and activate a virtual environment:
python -m venv env
source env/bin/activate # On Windows, use: env\Scripts\activate- Install required packages:
pip install pandas geopandas shapely- Download the Montgomery County GIS parcel shapefile from the Montgomery County GIS website and unzip it.
Convert a shapefile to CSV with centroid coordinates:
python process_shapefile.pyBy default, this looks for out.shp and creates output.csv. You can modify the input/output paths in the script.
The lookup_coordinates script now supports two commands: lookup and process.
# Search by address (default)
python lookup_coordinates.py lookup "123 MAIN ST"
# Search by partial address
python lookup_coordinates.py lookup "MAIN"
# Search by parcel ID
python lookup_coordinates.py lookup "R72 12307 0032" --field parcel_id
# Use a different csv file
python lookup_coordinates.py lookup "MAIN" --csv path/to/coordinates.csvpython lookup_coordinates.py process --csv output.csv --db parcels.db --output result.csvsearch_term: Address or parcel ID to search for--csv: Path to the CSV file (default: output.csv)--field: Field to search in ('address' or 'parcel_id', default: address)
--csv: Input CSV file path--db: SQLite database path (default: parcels.db)--output: Output CSV path (default: result.csv)--id-field: Parcel ID field name (default: TAXPINNO)
Convert the result CSV file to a JSON array:
python csv_to_json.py result.csv
# or specify custom output path
python csv_to_json.py result.csv --output data.jsonThe script will create a JSON file with the same name as the input CSV (but with .json extension) if no output path is specified.
The geocoder can run as a REST API (FastAPI) that returns parcel coordinates by parcel ID or address.
The API reads from a compact, indexed SQLite database. The simplest way to build it needs no shapefile and no GIS libraries — it pages through the County's live parcel layer and asks for centroids:
python fetch_parcels.py # County ArcGIS layer -> geocoder.db (~273k parcels, ~10 minutes)The older route still works if you have the shapefile-derived CSV:
python build_index.py # output.csv -> geocoder.db (~29MB)pip install -r requirements-web.txt
uvicorn app:app --reloadInteractive docs are served at http://localhost:8000/docs.
| Endpoint | Description |
|---|---|
GET /health |
Liveness check; returns parcel count |
GET /geocode?parcel_id=R72 12307 0032 |
Exact parcel ID lookup |
GET /geocode?address=4060 DELPHOS |
Partial, case-insensitive address lookup |
Example:
curl "http://localhost:8000/geocode?address=4060%20DELPHOS"
# {"count":1,"results":[{"parcel_id":"R72 12307 0032","address":"4060 DELPHOS AVE",
# "zip":"45402","latitude":39.763...,"longitude":-84.251...}]}Provide exactly one of parcel_id or address. Address matches are substrings,
so partial terms may return multiple results (capped at 50).
The production service runs on Railway from this repo's Dockerfile, which downloads
geocoder.db from the geocoder-data GitHub release. A monthly workflow
(.github/workflows/geocoder-data.yml) rebuilds the index from the County layer,
re-uploads it, and bumps LAST_BUILD so Railway redeploys. To refresh by hand:
python fetch_parcels.py
gh release upload geocoder-data geocoder.db --clobberIt is also the backend for the geocode tool in
OpenDayton.
The geocoder.db index is baked into the container image (read-only), so the
service is stateless and scales to zero when idle.
# Prereqs: gcloud CLI authenticated, a project selected, geocoder.db built.
./deploy.shTo run the container locally:
docker build -t parcel-geocoder .
docker run -p 8080:8080 parcel-geocoder
curl "http://localhost:8080/health"The script expects the following columns in the CSV file:
LOC_NBR: Street numberLOC_DIR: Street directionLOC_STREET: Street nameLOC_SUFFIX: Street suffixTAXPINNO: Parcel IDlatitude: Latitude coordinatelongitude: Longitude coordinate
About 190 parcels of 273,000 (0.07%) carry a street address that disagrees with
where the county's own parcel polygon sits. R72 08705 0033 is recorded as
20 W Fourth St but lies roughly a mile west, out where the 1000 block is.
Geocoding such an address returns the wrong location and nothing in the data
says so.
These are the county's records and this service passes them through unchanged —
inventing a corrected house number would be worse. Every build writes the
current list to address_outliers.csv (report_address_outliers in
fetch_parcels.py), which is the file to send the Auditor if you want them
fixed at source.
The check compares only within a taxing district. Montgomery County has many municipalities and most have a Main St, so comparing by street name alone flags thousands of parcels that are simply in another town.
- Address searches are case-insensitive and support partial matches
- Multiple matches will be displayed if found
- Invalid geometries in the shapefile will be skipped during processing
- The process command in lookup_coordinates.py joins the CSV data with a SQLite database
- The csv_to_json.py script handles NaN values and provides proper JSON formatting