A knowledge graph ecosystem for London, connecting housing data with transport, schools, and amenities using Neo4j.
Domain APIs own their data lifecycle (ingestion, enrichment, serving via REST + SQLite):
services/postcodes-api-- ONS postcode data (ONSPD)services/transport-api-- TfL station and line data with accessibilityservices/schools-api-- GIAS school data with Ofsted ratingsservices/properties-api-- Land Registry price paid data with price historyservices/crime-api-- Police UK crime data with LSOA-level yearly statisticsservices/imd-api-- Index of Multiple Deprivation (2025) at LSOA level
Graph Builder (graph/) collects from these APIs and builds a spatial knowledge graph in Neo4j, linking entities by proximity.
Query Engine (query-engine/) is an LLM-powered CLI that lets you ask natural language questions about the knowledge graph. It translates questions into Cypher queries, executes them, and synthesizes answers.
See docs/architecture.md for full details and docs/roadmap.md for planned work.
Everything you need to go from a fresh clone to a working graph with all data.
- Go 1.24+
- Docker (for Neo4j)
You need these files before starting. All are free/open data:
| Dataset | Source | Notes |
|---|---|---|
| ONSPD | ONS Geoportal | Download the CSV, ~1.3GB |
| Land Registry PPD | gov.uk | pp-complete.csv, ~4GB |
| Police UK crime | data.police.uk | Select Metropolitan Police + City of London, all dates |
| IoD 2025 File 7 | gov.uk | File_7_IoD2025_All_Ranks_Scores_Deciles_Population_Denominators.csv |
| TfL stations | Fetched live via TfL API | No download needed |
| GIAS schools | Fetched live via GIAS API | No download needed |
git clone https://github.com/mulder90/london.git
cd london
cp .env.example .env
# Edit .env if you want to change Neo4j password or portsmake docker-upmake build-allRun these from the repo root. Order matters — postcodes must be ingested first since properties-api uses the postcodes database for geocoding.
# 1. Postcodes (must be first — other services depend on it)
cd services/postcodes-api && make ingest FILE=/path/to/ONSPD.csv BOROUGH=London
cd ..
# 2. Transport (fetches from TfL API — needs TFL_API_KEY in .env for full data)
cd services/transport-api && make ingest
cd ..
# 3. Schools (fetches from GIAS API)
cd services/schools-api && make ingest-gias
cd ..
# 4. Properties (uses postcodes DB for geocoding)
cd services/properties-api && POSTCODES_DB_PATH=../postcodes-api/db/postcodes.db make ingest FILE=/path/to/pp-complete.csv
cd ..
# 5. Crime (import CSVs, then aggregate by year)
cd services/crime-api && make ingest-dir DIR=data/crime
cd services/crime-api && make aggregate YEAR=2023
cd services/crime-api && make aggregate YEAR=2024
cd services/crime-api && make aggregate YEAR=2025
cd ..
# 6. IMD
cd services/imd-api && make ingest FILE=/path/to/File_7_IoD2025.csv
cd ..Each API needs its own terminal:
# Terminal 1: Transport API (port 8080)
cd services/transport-api && make run
# Terminal 2: Schools API (port 8081)
cd services/schools-api && make run
# Terminal 3: Postcodes API (port 8082)
cd services/postcodes-api && make run
# Terminal 4: Properties API (port 8083)
cd services/properties-api && make run
# Terminal 5: Crime API (port 8084)
cd services/crime-api && make run
# Terminal 6: IMD API (port 8085)
cd services/imd-api && make runWith all 6 APIs running:
cd graph
# Build the graph CLI
make build
# Collect data from all APIs into staging JSON files
./bin/urbangraph collect all
# Apply Neo4j schema (constraints + indexes)
./bin/urbangraph schema
# Build everything: ingest nodes, create spatial links, aggregate stats
./bin/urbangraph build --link-all --aggregate-prices --aggregate-crime --aggregate-imd --years=2023,2024,2025# CLI queries (graph builder)
./bin/urbangraph query "W12 8BS" # postcode details
./bin/urbangraph query "W12 8BS" --show-nearby # nearby stations + schools
./bin/urbangraph query "W12 8BS" --show-nearby --nearby-type=Station # stations only
./bin/urbangraph query "W12 8BS" --show-nearby --radius=500 # within 500m
# Or open Neo4j Browser at http://localhost:7474 and run Cypher queries
# See docs/queries.md for example queriesThe query engine uses an LLM to translate natural language questions into Cypher queries against the graph.
# Configure LLM provider in .env (Groq free tier, OpenAI, or Ollama — see .env.example)
# Then:
make build-query-engine
# Interactive REPL
./query-engine/bin/qe
# Single question
./query-engine/bin/qe "What are the safest boroughs near a tube station?"
# Verbose mode (shows Cypher queries and tool calls)
./query-engine/bin/qe -v "Are prices going up or down in Wimbledon?"Example questions:
- "What are the safest boroughs near a tube station?"
- "Best area for a family with young kids near the District line?"
- "Are prices going up or down in Wimbledon?"
- "Compare schools in Richmond vs Hackney"
If you need to start from scratch (e.g. after schema changes):
# Wipe Neo4j data
make docker-down
docker rm london-neo4j 2>/dev/null
docker volume rm london_neo4j_data london_neo4j_logs london_neo4j_plugins 2>/dev/null
make docker-up
# Rebuild graph (APIs must be running)
cd graph
make build
./bin/urbangraph schema
./bin/urbangraph collect all
./bin/urbangraph build --link-all --aggregate-prices --aggregate-crime --aggregate-imd --years=2023,2024,2025london/
├── go.work # Go workspace
├── docker-compose.yml # Neo4j
├── Makefile # Root orchestration
├── docs/
│ ├── architecture.md # System architecture
│ ├── queries.md # Example Cypher queries
│ ├── roadmap.md # Development roadmap
│ └── future/ # Future design docs
├── services/
│ ├── postcodes-api/ # Postcode REST API (port 8082)
│ ├── transport-api/ # Transport REST API (port 8080)
│ ├── schools-api/ # Schools REST API (port 8081)
│ ├── properties-api/ # Properties & Prices REST API (port 8083)
│ ├── crime-api/ # Crime statistics REST API (port 8084)
│ └── imd-api/ # IMD deprivation REST API (port 8085)
├── graph/ # Graph builder (Neo4j)
└── query-engine/ # LLM-powered natural language query engine
| Target | Description |
|---|---|
build-all |
Build all projects |
test-all |
Run all tests |
clean-all |
Remove all build artifacts |
tidy-all |
Run go mod tidy on all modules |
docker-up |
Start Neo4j |
docker-down |
Stop Neo4j |
build-graph |
Build graph builder only |
build-postcodes |
Build postcodes API only |
build-transport |
Build transport API only |
build-schools |
Build schools API only |
build-properties |
Build properties API only |
build-crime |
Build crime API only |
build-imd |
Build IMD API only |
build-query-engine |
Build query engine |
test-query-engine |
Run query engine tests |
ingest-postcodes |
Ingest postcodes (requires FILE=) |
ingest-transport |
Ingest transport data from TfL API |
ingest-schools |
Ingest schools data from GIAS |
ingest-properties |
Ingest properties (requires FILE=) |
ingest-crime |
Ingest crime CSVs (requires DIR=) |
ingest-imd |
Ingest IMD data (requires FILE=) |
Contains HM Land Registry data Crown copyright and database right. This data is licensed under the Open Government Licence v3.0.