Daniel Gentile

Morphy

Cell tower lease analytics. Fifteen million parcels joined to tower records, two million of them filtered live in the browser.

2026. Code available on request

  • Python
  • DuckDB
  • GeoPandas
  • FastAPI
  • SQLAlchemy
  • Svelte 5
  • MapLibre GL
  • DuckDB-WASM
  • PMTiles
  • Fly.io

Problem

Cell towers sit on leased land, and the landowner under a tower is often worth talking to. No public dataset links a tower to that owner. The tower registrations are federal, the parcel and owner records are county by county in different shapes, and the economic context is in a third set of federal programs. Morphy builds that link so a person can rank towers and owners, pick the interesting ones, and work them as leads through a simple status ladder.

Approach

Four subsystems, each runnable on its own.

The data pipeline is registry driven. A new source is one file that declares how to normalise, join, and view itself, and every source declares an automation policy that the puller enforces, so a source that only allows manual download is never scraped. Sixteen sources are live. Outputs are GeoParquet and GeoJSON, plus vector tiles for parcels. Composing a state is chunked and memory bounded so Florida’s 11.4 million parcels fit in 16 gigabytes. DuckDB became the heavy-lift engine in a five-stage migration.

Owner resolution groups parcels by a hash of normalised name and address, strengthened by suffix stripping and token sorting. Across states, a name-plus-state key with a curated alias table handles the big tower companies. Phonetic matching was rejected because a false merge means calling the wrong person. A manual merge and split override is resolved in exactly one place.

The app is a Svelte 5 single-page app over MapLibre GL. Parcels come as vector tiles, and the filters run as SQL in DuckDB-WASM over Parquet files fetched by HTTP range request, all in the browser. An earlier Leaflet version could not handle a multi-million-parcel state. Three views: a dashboard, an explore map at tower grain, and a leads board at owner grain. TypeScript types for the API are generated from the backend’s OpenAPI schema and drift fails the build. The status ladder and filter semantics are JSON contract fixtures asserted from both Python and TypeScript.

The backend is FastAPI with SQLAlchemy. Every table carries a tenant identifier and every route filters by it. Credentials live in the database, an identity-only token rides an HTTP-only cookie, and the reverse proxy gates the static data files with the same check after a review found the composite was downloadable without signing in.

Tech

Python 3.11 with uv, GeoPandas, Shapely, PyArrow, DuckDB, FastAPI, SQLAlchemy, Alembic, and a Textual terminal interface for the puller. Svelte 5, Vite, MapLibre GL, PMTiles, DuckDB-WASM, Vitest, and Playwright on the front end. One always-on Fly.io machine: SQLite on a volume replicated by Litestream to object storage, Caddy in front, supervisord as the process tree. Four GitHub Actions workflows. About 48,000 lines, 76 decision records, 923 Python tests and 401 TypeScript tests. Four states live, about 15.8 million parcels composed.

My contribution

I built the whole technical side. The registry pattern, the automation policy as a legal contract, the dual output format, the chunked compose, the owner key and its override, the choice to move filtering into the browser, the tenant model, the auth hardening, and the single-machine deployment are my decisions, each with a record. I ran the architecture reviews that caught the open data plane. The code is private because parcel owner data is personal data at scale and needs clearing first; it is available on request.

AI usage

Nearly every commit carries a co-author trailer for the model that wrote it, across five models over five months. The repository has a rules file per subsystem, product requirement documents by issue number, and a code review graph wired in as a tool. I wrote the requirements and the decision records, reviewed the diffs, and ran the reviews that found the two biggest mistakes.

Lessons

The cross-state owner key had two producers, and the parity test pinned the wrong pair. A key change collapsed the map’s tower layer in production and needed a same-day fix. One producer per derived value, enforced by a test that compares them, is the rule now. The sign-in gate protected the application and left the data files open until an architecture review checked the data plane separately. And the first owner clustering was good enough for a demo and not for calling people, which is a different bar.

Back to projects