A commercial real estate intelligence tool that turns a street address into a one-page opportunity briefing. Enter an address and ParcelPulse pulls together parcel data, demographics, competitor density, and household spending estimates — the kind of cross-reference a CRE analyst would otherwise stitch together from five different tabs.
Live Demo: ParcelPulse
Address Search — Geoapify-powered autocomplete with 5-result dropdown, filtered to US addresses. The Geoapify key is proxied server-side so it never ships in the frontend bundle.
Interactive Map — Leaflet map with CartoDB Positron base tiles, occupying ~65% of the viewport. Renders the searched location, parcel polygons, demographic boundaries, and color-coded competitor markers in stacked GeoJSON layers.
Parcel Data — NC OneMap integration returns owner name, land use classification, acreage, and the parcel polygon itself. Land use is color-coded on the map: blue for commercial, gray for residential, amber for vacant. Non-NC addresses get a graceful "coverage not available" response so the rest of the panels still work.
Demographics Panel — US Census ACS 5-Year data: median household income, household count, age distribution, and education attainment. The search lat/lng is resolved to a 15-digit block FIPS code via the FCC Area API, then a fallback chain tries block group → tract → county so dense urban and sparse rural addresses both return something usable. The chosen geography boundary is rendered on the map.
Competitors — Overpass API (OpenStreetMap) pulls nearby supermarkets, restaurants, gas stations, retail, and other categories. A density score (High > 50, Medium 20–50, Low < 20) gives a one-glance read on commercial saturation. Categories can be toggled individually from the sidebar.
Spending Potential — BLS Consumer Expenditure Survey data, joined to the demographics panel's income and household count, estimates annual household spending across categories (food, housing, transportation, etc.). Pre-processed BLS JSON ships with the app; null cells in the source data are filled via linear interpolation.
Response Caching & Rate Limiting — All external API responses are cached in PostgreSQL (api_cache) with per-service TTLs: 30 days for parcels, 7 days for competitors, etc. A daily-cap rate limiter (api_usage) enforces per-service budgets (500/day for NC OneMap, 2,500/day for Geoapify) to keep the app inside free-tier limits.
| Layer | Technology |
|---|---|
| Frontend | React 19, Vite, TypeScript, Tailwind CSS v4 |
| Maps | Leaflet, react-leaflet, CartoDB Positron tiles |
| Charts | Chart.js, react-chartjs-2 |
| Backend | Python 3.12, FastAPI (async), SQLAlchemy 2.0, Alembic |
| Database | PostgreSQL 16 (asyncpg driver) |
| HTTP client | httpx (async) |
| Rate limiting | SlowAPI |
| Data sources | Geoapify, NC OneMap, Census Bureau ACS, TIGERweb, Overpass, BLS |
| Deployment | Docker Compose for local Postgres |
ParcelPulse stitches together seven external data sources. All are free; two require an API key.
| Service | Endpoint | Auth | Limits | Used For |
|---|---|---|---|---|
| Geoapify Geocoding Autocomplete | api.geoapify.com/v1/geocode/autocomplete |
API key (server-side proxy) | 3,000 req/day free tier; app-side cap 2,500/day | Address search / autocomplete dropdown |
| NC OneMap Parcels (ArcGIS REST) | services.nconemap.gov/secure/rest/services/NC1Map_Parcels/FeatureServer |
None | App-side cap 500/day | Parcel polygons, owner, land use, acreage (NC only) |
| FCC Area API | geo.fcc.gov/api/census/block/find |
None | No published rate cap | Lat/lng → 15-digit block FIPS code (state, county, tract, block group) |
| US Census Bureau ACS 5-Year | api.census.gov/data |
API key | No published rate cap | Income, household count, age, education at block group / tract / county |
| Census TIGERweb | tigerweb.geo.census.gov/arcgis/rest/services |
None | No published rate cap | Geography boundary polygons (block group=10, tract=8, county=84) |
| Overpass API (OpenStreetMap) | overpass-api.de/api/interpreter |
None | Shared-instance fair-use | Nearby competitors / POI by category |
| BLS Consumer Expenditure Survey | Pre-processed static JSON (backend/data/bls_spending.json) |
n/a | n/a | Household spending estimates by income tier |
Map tiles are served from CartoDB Positron ({a,b,c}.basemaps.cartocdn.com/light_all/) — not a JSON API, but worth flagging as an external dependency.
This is a monorepo with the frontend and backend as separate applications.
parcelpulse/
├── backend/ # FastAPI app
│ ├── app/
│ │ ├── main.py # App factory, JSON logging, CORS, rate limiter
│ │ ├── config.py # Pydantic settings (env vars)
│ │ ├── database.py # Async SQLAlchemy engine + session
│ │ ├── rate_limit.py # SlowAPI limiter (30/minute global)
│ │ ├── models/ # SQLAlchemy: ApiCache, ApiUsage (only)
│ │ ├── schemas/ # Pydantic request/response models
│ │ ├── routes/api.py # All /api/* endpoints
│ │ └── services/ # External API integrations + cache layer
│ ├── data/ # BLS spending JSON (pre-processed)
│ ├── alembic/ # Database migrations
│ └── tests/ # Pytest suite
├── frontend/ # React + Vite app
│ └── src/
│ ├── components/ # Map, SearchBar, Sidebar, *Panel components
│ ├── pages/ # HomePage
│ └── api/ # Fetch wrapper for the backend
├── docker-compose.yml # Local Postgres
├── SPEC.md # Full product spec
└── CLAUDE.md # Sprint progress + session notes
ParcelPulse is intentionally scoped: parcel data is North Carolina only (NC OneMap is the underlying source). Addresses outside NC return a {coverage: false} response on the parcel endpoint, but demographics, competitor density, and spending data work nationwide because they ride on Census, OpenStreetMap, and BLS — all national datasets. A non-NC search still gets a useful four-out-of-five panel briefing.
ParcelPulse is a stateless public demo — there are no users, no projects, no saved searches. The only persistent state is the integration scaffolding: an api_cache table (key, service, response JSON, expiry) and an api_usage table (service, calls_today, date) that drives the daily rate limiter. All real data lives in the external APIs and is fetched on demand, then cached.
- Python 3.12+
- Node.js 18+
- Docker (for local PostgreSQL)
- A free Geoapify API key and a free Census API key
docker compose up -dThis starts a PostgreSQL 16 container bound to 127.0.0.1:5432 with a persistent volume.
cd backend
python3 -m venv .venv
source .venv/bin/activate
pip install -r requirements.txtCreate a .env file in the backend directory (template in backend/.env.example):
DATABASE_URL=postgresql+asyncpg://parcelpulse:devpassword@localhost:5432/parcelpulse
FRONTEND_URL=http://localhost:5173
GEOAPIFY_API_KEY=your-geoapify-api-key
CENSUS_API_KEY=your-census-api-key
NCONEMAP_BASE_URL=https://services.nconemap.gov/secure/rest/services/NC1Map_Parcels/FeatureServer
NCONEMAP_DAILY_CAP=500
GEOAPIFY_DAILY_CAP=2500
Run migrations and start the server:
alembic upgrade head
uvicorn app.main:app --reloadThe API is available at http://localhost:8000 with interactive docs at http://localhost:8000/docs.
cd frontend
npm installCreate a .env file in the frontend directory:
VITE_API_URL=http://localhost:8000
Geoapify is proxied through the backend, so no API key is needed in the frontend bundle.
Start the dev server:
npm run devThe app is available at http://localhost:5173.
FastAPI auto-generates interactive API documentation. With the backend running, visit /docs for the Swagger UI. Endpoints:
GET /api/geocode— Geoapify autocomplete proxy (server-side key)POST /api/search— Combined search; fans out to demographics, competitor, and spending services with graceful per-service fallback (a failing service returnsnullrather than breaking the response)GET /api/demographics— Census ACS data + TIGERweb boundary for a lat/lngGET /api/competitors— Overpass competitor data for a lat/lngGET /api/parcels— NC OneMap parcel data for a lat/lng + state code (NC only; other states return a no-coverage response)GET /api/spending— BLS spending estimates given income and household countGET /api/usage— Per-service daily call countsGET /health— Healthcheck
The app is stateless and has no auth surface, but it still ships with sensible defaults: SlowAPI rate limiting (30 requests/minute) on every endpoint, CORS locked to the configured frontend origin, GEOID values validated against ^\d{5,15}$ before being interpolated into ArcGIS queries, exception logs sanitized to error class names (no URL or PII leakage), no third-party API keys in the frontend bundle (Geoapify is proxied), and per-service daily caps that hard-stop requests when free-tier budgets are exhausted.
This app was designed, built, and deployed using Claude Code as an AI coding agent. SPEC.md served as the application blueprint, and the work was broken into sprints (map/geocoding → demographics → competitors → parcels → spending), each driven iteratively through Claude. The commit history reflects that incremental development process.