Geographic Health Intelligence API — census-tract-level social determinants of health
Project description
GeoHealth Context API
Census-tract-level geographic health intelligence API. Given a street address or lat/lng coordinates, returns demographics, CDC/ATSDR Social Vulnerability Index (SVI) themes, CDC PLACES health outcome measures, and an optional AI-generated narrative for the surrounding census tract.
Features
- Address and coordinate geocoding (Census Bureau + Nominatim fallback)
- Census tract resolution via PostGIS spatial queries or FIPS-code lookup
- ACS demographics: population, income, poverty, insurance, unemployment, age
- CDC/ATSDR SVI theme percentile rankings (4 themes)
- CDC PLACES health outcome measures (crude prevalence)
- Composite SDOH index
- AI-generated narrative summaries (Anthropic Claude)
- Batch address lookups, spatial radius search, tract comparison
- Per-key sliding-window rate limiting with standard headers
- SHA-256 API key hashing
Quick Start
1. Clone and start services
git clone https://github.com/RussellStover1983/geohealth-api.git
cd geohealth-api
docker compose up --build -d
This launches PostgreSQL/PostGIS and the API on http://localhost:8000.
2. Load census tract data
# Single state (fast, good for development)
pip install -e ".[etl]"
python -m geohealth.etl.load_all --state 27
# All states (resumable)
python -m geohealth.etl.load_all --state all --resume
3. Verify
curl http://localhost:8000/health
# {"status":"ok","database":"connected","detail":null}
Interactive API Docs
Once running, explore the full API interactively:
- Swagger UI: http://localhost:8000/docs
- ReDoc: http://localhost:8000/redoc
- OpenAPI JSON: http://localhost:8000/openapi.json
Authentication
Most endpoints require an API key via the X-API-Key header.
curl -H "X-API-Key: your-key-here" "http://localhost:8000/v1/context?address=..."
Configuration:
| Variable | Description |
|---|---|
AUTH_ENABLED |
Set to true to enforce key validation (default: false) |
API_KEYS |
Comma-separated list of valid keys |
Keys can be provided as plaintext or as pre-hashed SHA-256 hex strings. The API always hashes keys with SHA-256 before comparison — plaintext keys are never stored or logged.
Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
No | API and database connectivity check |
| GET | /v1/context |
Yes | Primary lookup — address or lat/lng to tract data |
| POST | /v1/batch |
Yes | Batch address lookup (up to BATCH_MAX_SIZE) |
| GET | /v1/nearby |
Yes | Find census tracts within a radius |
| GET | /v1/compare |
Yes | Compare two tracts or tract vs. averages |
| GET | /v1/dictionary |
Yes | Data dictionary — field definitions with clinical context |
| GET | /v1/stats |
Yes | Per-state data loading statistics |
| GET | /metrics |
No | Application metrics (counters, latency, cache stats) |
| GET | /llms.txt |
No | Agent-readable API overview (llmstxt.org) |
| GET | /llms-full.txt |
No | Full agent-readable reference with clinical context |
Example Requests
Context lookup by address
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/context?address=1234+Main+St,+Minneapolis,+MN+55401"
Context lookup by coordinates with narrative
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/context?lat=44.9778&lng=-93.265&narrative=true"
Batch lookup
curl -X POST -H "X-API-Key: test" -H "Content-Type: application/json" \
-d '{"addresses":["1234 Main St, Minneapolis, MN","456 Oak Ave, St Paul, MN"]}' \
http://localhost:8000/v1/batch
Nearby tracts
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/nearby?lat=44.9778&lng=-93.265&radius=5&limit=10"
Compare tracts
# Two tracts
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/compare?geoid1=27053026200&geoid2=27053026300"
# Tract vs. state average
curl -H "X-API-Key: test" \
"http://localhost:8000/v1/compare?geoid1=27053026200&compare_to=state"
Statistics
curl -H "X-API-Key: test" "http://localhost:8000/v1/stats"
Python SDK
The project includes a typed Python SDK that wraps every endpoint with both async and sync clients.
Async usage
from geohealth.sdk import AsyncGeoHealthClient
async with AsyncGeoHealthClient("http://localhost:8000", api_key="your-key") as client:
result = await client.context(address="1234 Main St, Minneapolis, MN 55401")
print(result.tract.geoid, result.tract.poverty_rate)
nearby = await client.nearby(lat=44.9778, lng=-93.265, radius=3.0)
for tract in nearby.tracts:
print(tract.geoid, tract.distance_miles)
Sync usage
from geohealth.sdk import GeoHealthClient
with GeoHealthClient("http://localhost:8000", api_key="your-key") as client:
result = client.context(lat=44.9778, lng=-93.265)
print(result.tract.geoid)
Error handling
from geohealth.sdk import AsyncGeoHealthClient, RateLimitError, AuthenticationError
async with AsyncGeoHealthClient("http://localhost:8000", api_key="your-key") as client:
try:
result = await client.context(address="123 Main St")
except RateLimitError as exc:
print(f"Rate limited! Resets in {exc.rate_limit_info.reset}s")
except AuthenticationError as exc:
print(f"Auth failed: {exc.detail}")
MCP Server (for AI Agents)
The GeoHealth MCP server exposes all API endpoints as native tools for Claude Desktop, Claude Code, and other MCP-compatible agents. No HTTP wiring needed — agents call tools directly.
Install and run
pip install geohealth-api[mcp]
GEOHEALTH_API_KEY=your-key python -m geohealth.mcp
Claude Desktop configuration
Add to your claude_desktop_config.json:
{
"mcpServers": {
"geohealth": {
"command": "python",
"args": ["-m", "geohealth.mcp"],
"env": {
"GEOHEALTH_BASE_URL": "https://geohealth-api-production.up.railway.app",
"GEOHEALTH_API_KEY": "your-api-key"
}
}
}
}
Available tools
| Tool | Description |
|---|---|
lookup_health_context |
Primary lookup — address/coords to tract demographics, SVI, PLACES |
batch_health_lookup |
Multi-address lookup (up to 50) |
find_nearby_tracts |
Spatial radius search |
compare_tracts |
Compare tracts or tract vs averages |
get_data_dictionary |
Field definitions with clinical interpretation |
get_tract_statistics |
Data coverage by state |
Rate Limiting
Every authenticated response includes rate-limit headers:
| Header | Description |
|---|---|
X-RateLimit-Limit |
Maximum requests per window |
X-RateLimit-Remaining |
Requests remaining in current window |
X-RateLimit-Reset |
Seconds until the window resets |
When the limit is exceeded, the API returns 429 Too Many Requests with the same headers. Configure via RATE_LIMIT_PER_MINUTE (default: 60).
Environment Variables
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
— | PostgreSQL connection string (postgresql+asyncpg://...) |
AUTH_ENABLED |
false |
Enable API key authentication |
API_KEYS |
— | Comma-separated valid keys (plaintext or SHA-256 hex) |
CORS_ORIGINS |
* |
Comma-separated allowed origins |
RATE_LIMIT_PER_MINUTE |
60 |
Max requests per key per minute |
ANTHROPIC_API_KEY |
— | Anthropic API key for narrative generation |
BATCH_MAX_SIZE |
50 |
Maximum addresses per batch request |
CACHE_MAXSIZE |
4096 |
LRU cache maximum entries |
CACHE_TTL |
3600 |
Cache time-to-live in seconds |
RUN_MIGRATIONS |
true |
Run Alembic migrations on startup |
LOG_FORMAT |
text |
text for human-readable, json for structured JSON |
LOG_LEVEL |
INFO |
Standard Python log levels |
Observability
Structured Logging
The API supports two log formats controlled by LOG_FORMAT:
text(default) — Human-readable format with optional request ID prefix:2024-01-15 12:00:00 INFO [abcdef123456] geohealth.access - GET /health 200 1.23msjson— Single-line JSON for log aggregators withtimestamp,level,logger,message,request_id, and optionalexceptionfields
Request Correlation
Every request gets an X-Request-ID header. If the client sends one, it is echoed back; otherwise a new UUID is generated. The request ID is included in all log lines and is available to downstream services for distributed tracing.
Application Metrics
GET /metrics returns a JSON snapshot of application metrics:
- Request counters — total requests, status code breakdown
- Cache stats — hits, misses, hit rate, current size
- Geocoder stats — Census/Nominatim success and failure counts
- Narrative stats — success and failure counts
- Latency percentiles — p50, p90, p95, p99
Enhanced Health Check
GET /health now includes cache, rate limiter, and uptime information when the database is healthy.
Deployment (Railway)
The API is configured for deployment on Railway using the existing multi-stage Dockerfile.
Prerequisites
- A Railway account
- The GitHub repo linked to your Railway project
1. Create a Railway project
- In the Railway dashboard, click New Project → Deploy from GitHub Repo
- Select the
geohealth-apirepository - Railway will detect the
railway.tomland build from the Dockerfile automatically
2. Add a PostGIS database service
Railway's managed Postgres does not include PostGIS binaries, so add a custom Docker service:
- In your Railway project, click New → Docker Image
- Set the image to
postgis/postgis:16-3.4 - Add a persistent volume mounted at
/var/lib/postgresql/data - Add these environment variables to the PostGIS service:
POSTGRES_USER=geohealthPOSTGRES_PASSWORD=<strong-password>POSTGRES_DB=geohealth
- Note the service's internal hostname (e.g.,
postgis.railway.internal) and port
3. Configure environment variables on the API service
Set these variables on the API service (not the PostGIS service):
| Variable | Value |
|---|---|
DATABASE_URL |
postgresql+asyncpg://geohealth:<password>@${{PostGIS.RAILWAY_PRIVATE_DOMAIN}}:5432/geohealth |
DATABASE_URL_SYNC |
postgresql://geohealth:<password>@${{PostGIS.RAILWAY_PRIVATE_DOMAIN}}:5432/geohealth |
AUTH_ENABLED |
true |
API_KEYS |
Comma-separated SHA-256 hashes (see step 6) |
CORS_ORIGINS |
Your frontend domain(s), e.g., https://app.example.com |
LOG_FORMAT |
json |
ANTHROPIC_API_KEY |
(optional) Your Anthropic key for AI narrative generation |
4. Deploy and verify
Railway auto-deploys on push to master. After the build completes:
curl https://<your-app>.railway.app/health
# {"status":"ok","database":"connected","detail":null}
5. Load census data
Run the ETL from your local machine against Railway's public database URL (found in the PostGIS service's networking settings):
pip install -e ".[etl]"
DATABASE_URL_SYNC=postgresql://geohealth:<password>@<public-host>:<port>/geohealth \
python -m geohealth.etl.load_all --state 27
For all states (resumable):
DATABASE_URL_SYNC=postgresql://geohealth:<password>@<public-host>:<port>/geohealth \
python -m geohealth.etl.load_all --state all --resume
6. Generate and distribute API keys
Generate a random key and its SHA-256 hash:
python -c "
import secrets, hashlib
key = secrets.token_urlsafe(32)
hashed = hashlib.sha256(key.encode()).hexdigest()
print(f'Give to client: {key}')
print(f'Add to API_KEYS: {hashed}')
"
Add the hash to API_KEYS in Railway's environment variables (comma-separated if multiple). Distribute the plaintext key to the API consumer — the API never stores or logs it.
Development
Local setup
pip install -e ".[dev]" # Core + test deps
pip install -e ".[dev,etl]" # Include ETL deps (geopandas, shapely, etc.)
Run the dev server
docker compose up -d db # Start PostGIS only
uvicorn geohealth.api.main:app --reload
Tests
pytest # All tests (no live DB required)
pytest tests/test_context.py -v # Single module
pytest -k test_auth # Pattern match
Lint
ruff check geohealth/ tests/ # line-length=99, target py311
Migrations
alembic upgrade head # Apply all
alembic revision --autogenerate -m "description" # Generate new
alembic stamp head # Mark existing DB as current
Project Structure
geohealth/
├── api/ # FastAPI app, routes, schemas, middleware, auth
│ └── routes/ # Endpoint modules (context, batch, nearby, compare, stats)
├── services/ # Geocoding, tract lookup, cache, rate limiter, narrator
├── etl/ # Data loading (TIGER/Line, ACS, SVI, PLACES)
├── db/ # SQLAlchemy models and async session
├── migrations/ # Alembic migration versions
└── config.py # Settings via pydantic-settings
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file geohealth_api-0.1.0.tar.gz.
File metadata
- Download URL: geohealth_api-0.1.0.tar.gz
- Upload date:
- Size: 75.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e2ecd2265e6d1cfeee00c3abc51291a1228a0f54825b5ce0094b74e7453106e
|
|
| MD5 |
97aed9da3a6d28ae663354ac2fb3c935
|
|
| BLAKE2b-256 |
5f110d8dd624baaf1f11b93004ddd5c83f79e6317446180ef20bc417d903eccc
|
File details
Details for the file geohealth_api-0.1.0-py3-none-any.whl.
File metadata
- Download URL: geohealth_api-0.1.0-py3-none-any.whl
- Upload date:
- Size: 71.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a63f9f5201e41b0d83c50cbef2c6c3946b3277df4ced998c44347a22e140daf8
|
|
| MD5 |
8188da00d125e5c295454b1c9d5a619d
|
|
| BLAKE2b-256 |
16a5b58d48dbcf51747e333af5eec06d630f9b6dafef62c21043b348dd43f16d
|