TALON (Topographic Analysis & Land Ownership Navigator)
Role: Solo, end-to-end. architecture, backend, frontend, data engineering, ML pipelines, infrastructure, product
Tech Stack: Python (FastAPI), PostgreSQL (PostGIS), Astro/Preact, MapLibre GL, Three.js/Potree, OpenAI API, Docker
Live Site: Coming soon. Staging is up on self-hosted infrastructure; open beta once the go-live list is worked through
Overview
TALON is a land acquisition intelligence system that combines geospatial analysis, machine learning, and conversational AI to evaluate real estate parcels for development, investment, and conservation.
Consumer maps show you where things are. TALON calculates whatâs possible. It ingests raw government datasets (tax parcels, zoning vectors, LiDAR elevation models, soil surveys, satellite imagery, forestry data) and enriches every parcel with dozens of derived metrics: slope exposure by compass direction, canopy density, timber volume, soil suitability, and more. You can explore the data through natural-language conversation (âFind me a 10-acre lot with southern exposure, mature hardwood, and road accessâ) or through preset filters tuned for specific use cases, solar development, conservation, timber investment, and so on.
Coverage is currently focused on 14 western North Carolina counties at 1m LiDAR resolution, ahead of an open beta. Earlier iterations spread across North Carolina, Massachusetts, Vermont, and several Virginia counties; I deliberately descoped to guarantee consistent depth on every county I ship rather than inconsistent breadth across states.
System Architecture
TALON uses hexagonal architecture (Ports & Adapters) with strict dependency inversion. The domain layer is pure business logic with zero framework imports, surrounded by application services, abstract port interfaces, and concrete adapters for the database, APIs, and web layer.
graph TD
subgraph "Data Pipeline (Offline)"
A[Raw Data: GeoPackages, LiDAR, Soil, Satellite] --> B{Python ETL Engine}
B -->|GeoPandas/Shapely| C[Clean & Reproject to EPSG:4326]
B -->|Rasterio/laspy| D[Slope, Elevation & Forestry Analysis]
B -->|Multiprocessing| E[Spatial Joins & Enrichment]
E --> F[(PostgreSQL + PostGIS)]
end
subgraph "Intelligence Layer"
F --> G[FastAPI Async Backend]
G --> H[LLM Conversational Engine]
G --> I[ML Owner Classifier]
G --> J[LiDAR Source Orchestrator]
end
subgraph "Application Layer"
G --> K[Astro SSR + Preact Islands]
K --> L[MapLibre GL Visualization]
K --> M[Three.js / Potree 3D Viewer]
end
Components
- The Spatial Engine: PostgreSQL with PostGIS handles geometric queries (intersections, containment, distance buffers) that standard SQL just canât. R-Tree spatial indexes keep even complex queries fast.
- The ETL Pipeline: a Python processor that ingests massive GeoPackage, LiDAR (LAZ/COPC), soil survey, and satellite datasets. Normalizes coordinate systems, calculates slope/aspect per parcel, derives forestry metrics (canopy %, timber volume in board feet, stumpage value), and scores soil suitability.
- The Conversational Engine: LLM with function calling interprets natural-language queries into structured spatial searches, with multi-turn refinement and context-aware follow-ups.
- The Frontend: Astro SSR application using Preact islands for interactive components, Nanostores for lightweight atomic state, and MapLibre GL for rendering thousands of parcel boundaries without lag.
How It Works
1. Conversational AI Search
Geospatial databases are powerful but demand specialized query knowledge. A user shouldnât need to know PostGIS to find land. So I built a multi-turn conversational search powered by LLM function calling.
- The LLM selects from dynamically-generated tool schemas:
search_with_assumptions,refine_previous_search,ask_for_clarification, andget_statistics. - System prompts include field-level guidance (terrain aspect conventions, forestry metric definitions, statistical operators) so the model makes informed filter decisions instead of guessing.
- Conversation state persists in PostgreSQL, which is what enables follow-ups like ânow narrow that to lots with road frontageâ without restating prior context.
- A separate Parcel Chat endpoint handles focused Q&A about a specific property with its full context pre-loaded.
- Property Summarizer generates narrative descriptions (brief or detailed) from raw parcel data: elevation, buildings, sales history, terrain.
2. Multi-Source LiDAR Orchestration
LiDAR data comes from multiple government sources with different formats, quality levels, and coverage. Users shouldnât have to know which source covers their area, or even that there are multiple sources. So I built a 3-tier discovery and extraction system with smart fallback.
- Tier 1, 3DEP (USGS OpenTopo): Cloud-Optimized Point Clouds streamed via STAC catalog. Highest quality, tile-based.
- Tier 2, TNM (The National Map): raw LAZ tiles. Full fallback when 3DEP lacks coverage.
- Tier 3, Local LAZ Mirror: cached files for development and instant extraction.
discover_and_reconcile() queries all tiers in parallel, ranks results by quality level and recency, and presents unified options. Deterministic caching (keyed by tier + survey date + name) prevents redundant downloads, and a stale-job watchdog sweeps stuck processing tasks. Extracted point clouds feed into a Potree-based 3D viewer with TPI (Topographic Position Index) and HAG (Height Above Ground) channel overlays.
3. High-Compute Spatial Processing
Calculating the average slope of a 100-acre parcel from raw LiDAR is computationally expensive. Doing it for tens of thousands of parcels takes hours if you do it naively. So I parallelized the pipeline with Pythonâs ProcessPoolExecutor.
- Datasets are chunked into geographic partitions for parallel spatial intersection using GeoPandas and Shapely.
- Every parcel gets slope broken into directional bins, flat, gentle, moderate, steep acres across 8 compass directions for exposure analysis.
- Forestry enrichment calculates canopy cover percentage, P95 tree height, estimated board feet, and stumpage value.
4. ML-Powered Owner Classification
Raw parcel ownership data is messy. Names donât tell you whether an owner is an individual, a corporation, a trust, or a government entity. So I built a scikit-learn classification pipeline with an LLM-assisted labeling stage.
- OpenAI Batch API generates training labels from owner names at scale (cheap, because Batch).
- Classifier categorizes ownership types, which enables filters like âfind absentee corporate-owned parcels.â
5. Hybrid Rendering Frontend
The UI needs both server-rendered pages (for auth, SEO, fast initial load) and highly interactive components (map, chat, 3D viewer). Astro SSR with Preact islands gets both.
- Static page shells render server-side. Interactive components hydrate as Preact islands (
client:load). - Nanostores provides atomic, framework-agnostic state, no prop drilling, minimal overhead.
- MapLibre GL and the vanilla DOM sidebar talk via CustomEvents, keeping the map layer decoupled from the component tree.
- API calls proxy through SSR routes with httpOnly cookies and transparent token refresh.
Code Snippet: Conversational Search Orchestration
This is how the chat search use case coordinates conversation state, the LLM, and tool execution.
async def execute(self, user_id: UUID, message: str,
conversation_id: Optional[UUID] = None) -> ChatSearchResponse:
# Load or create conversation with full message history
conversation = await self._get_or_create_conversation(
user_id, conversation_id
)
# Build context-aware prompt with field descriptions and prior filters
messages = self._build_messages(conversation, message)
# LLM selects a tool and generates structured arguments
llm_response = await self.llm_service.chat_with_tools(
messages=messages,
tools=self.tool_definitions, # Dynamically generated from schema
temperature=0.7,
)
# Execute the chosen tool (search, refine, clarify, or stats)
result = await self._execute_tool_call(llm_response.tool_call)
# Persist conversation state for multi-turn refinement
await self._save_turn(conversation, message, llm_response, result)
return result
Deployment Journey
TALON was developed on a beefy local workstation, essential for processing LiDAR datasets and running the full PostGIS + Redis + application stack during development. The first deployment target was Render, which got the app online quickly but made the limitations of managed PaaS painfully obvious for compute-heavy geospatial work. It now runs on a self-managed Proxmox host on a bare metal dedicated server: significantly more CPU, memory, and storage at a marginal cost bump, full control over the environment, and actual headroom to scale as data coverage expands.
Infrastructure:
- Docker Compose orchestrating Backend (Python 3.11 + GDAL), Frontend (Node 20 + vendored Potree), PostgreSQL 17/PostGIS 3.5, and Redis 7
- GitHub Actions CI/CD with branch flow (
feature/*âdevelopâstagingâmain), automated linting (black, flake8, mypy), security scanning (Bandit), and test suite execution against a live PostGIS instance - Sentry for error tracking and performance monitoring across both frontend and backend
Features
- Conversational Property Search: natural-language queries with multi-turn refinement via LLM function calling
- AI Property Summaries: narrative descriptions generated from raw parcel data
- Parcel Chat: focused Q&A about individual properties with full context pre-loaded
- 3D Terrain Viewer: LiDAR point cloud visualization with Potree, including TPI and HAG overlays
- Multi-Source LiDAR: automatic discovery and extraction from 3DEP, TNM, and local sources with quality-based ranking
- Preset Search Library: 20+ curated filters for common use cases (buildable lots, solar potential, timber investment, conservation, estate parcels)
- Dynamic Filtering: auto-generated filter panel from database schema with real-time map updates
- ML Owner Classification: trained classifier categorizing ownership types from raw name data
- Forestry Analytics: canopy cover, tree height, board feet, stumpage value per parcel
- Soil & Environmental: SSURGO soil suitability, wetland identification, NDVI vegetation indexing
- Topographic Binning: slope broken into flat/gentle/moderate/steep acres across 8 compass directions
Tech Stack
- Languages: Python 3.11+, TypeScript
- Geospatial: PostGIS, GeoPandas, Rasterio, Shapely, PyProj, laspy, copclib, GDAL
- Backend: FastAPI (async), SQLAlchemy/SQLModel, Alembic, Redis, Pydantic
- AI/ML: OpenAI API (function calling), scikit-learn, OpenAI Batch API
- Frontend: Astro 5 (SSR), Preact 10, Nanostores, MapLibre GL JS 5, Three.js, Potree
- Testing: Pytest (897+ tests), Vitest, Playwright, 95%+ domain coverage
- Infrastructure: Docker Compose, GitHub Actions CI/CD, Nginx, Sentry
- Data Sources: USGS 3DEP, OpenStreetMap, NLCD, SSURGO, Sentinel-2, National Wetlands Inventory, County GIS