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
- 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