perfect-postcode/README.md
2026-05-17 10:16:30 +01:00

230 lines
7.1 KiB
Markdown

# Property Map
Interactive UK property intelligence map. The app combines transaction, EPC,
postcode, neighbourhood, transport, POI, and travel-time data into local parquet
files, serves fast geospatial aggregations from Rust, and renders the result as
a React/deck.gl map.
The public product is branded as Perfect Postcodes, while this repository is
still named `property-map`.
## Public SEO Pages
The indexable public pages are listed in `frontend/public/sitemap.xml` and
prerendered by `frontend/scripts/prerender.mjs`:
- [Home](https://perfect-postcode.co.uk/) - `/`
- [Learn](https://perfect-postcode.co.uk/learn) - `/learn`
- [Pricing](https://perfect-postcode.co.uk/pricing) - `/pricing`
- [Property price map](https://perfect-postcode.co.uk/property-price-map) -
`/property-price-map`
- [Postcode property search](https://perfect-postcode.co.uk/postcode-property-search) -
`/postcode-property-search`
- [Commute property search](https://perfect-postcode.co.uk/commute-property-search) -
`/commute-property-search`
- [School property search](https://perfect-postcode.co.uk/school-property-search) -
`/school-property-search`
- [Postcode checker](https://perfect-postcode.co.uk/postcode-checker) -
`/postcode-checker`
- [Birmingham property search](https://perfect-postcode.co.uk/property-search/birmingham) -
`/property-search/birmingham`
- [Manchester property search](https://perfect-postcode.co.uk/property-search/manchester) -
`/property-search/manchester`
- [Bristol property search](https://perfect-postcode.co.uk/property-search/bristol) -
`/property-search/bristol`
- [Data sources](https://perfect-postcode.co.uk/data-sources) - `/data-sources`
- [Methodology](https://perfect-postcode.co.uk/methodology) - `/methodology`
- [Privacy and security](https://perfect-postcode.co.uk/privacy-security) -
`/privacy-security`
## What Is In Here
- `frontend/` - React 18, TypeScript, Tailwind, MapLibre, and deck.gl. The app
has a landing page, map dashboard, saved searches/properties, account pages,
pricing, invites, and shareable URLs.
- `server-rs/` - Rust Axum API. It loads the generated parquet data into memory,
builds spatial indexes, serves H3/postcode aggregations, proxies PocketBase,
serves PMTiles, handles AI filter parsing, screenshots, exports, checkout, and
telemetry.
- `pipeline/` - Python/Polars download and transform pipeline. `Makefile.data`
orchestrates the data DAG.
- `r5-java/` - Batch travel-time generator using Conveyal R5. It writes sparse
per-destination parquet files for car, bicycle, walking, and transit.
- `screenshot/` - Playwright/Express service used by the Rust API for map
screenshots and Open Graph images.
- `property-data/` and `manual-data/` - Local generated/downloaded data. These
are runtime inputs, not source code.
## Runtime Data
The Rust server expects these files or directories to exist:
```text
property-data/properties.parquet
property-data/postcode.parquet
property-data/filtered_uk_pois.parquet
property-data/places.parquet
property-data/uk.pmtiles
property-data/postcode_boundaries/
property-data/travel-times/
```
Most data, including police.uk crime archives, can be downloaded or generated
through `Makefile.data`. Some inputs are deliberately manual:
- `manual-data/domestic-csv.zip` from the EPC register
- postcode boundaries, generated from OA boundaries, INSPIRE polygons, and UPRN
lookup data
Build the main property datasets with:
```bash
uv sync
make -f Makefile.data prepare
make -f Makefile.data tiles
make -f Makefile.data download-places
make -f Makefile.data generate-postcode-boundaries
```
`generate-postcode-boundaries` writes to `manual-data/postcode_boundaries/`.
The running server expects the same structure under
`property-data/postcode_boundaries/`; copy or symlink it if needed.
Travel times are built separately because they are expensive:
```bash
make -f Makefile.data generate-travel-times
```
For a quick R5 smoke test:
```bash
./r5-java/run.sh --demo
```
## Local Development
With the required files in `property-data/`, the full stack can be started with
Docker Compose:
```bash
docker compose up --build
```
Services:
- frontend: http://localhost:3001
- API: http://localhost:8001
- PocketBase: http://localhost:8090
- screenshot service: http://localhost:8002
The frontend dev server proxies `/api` and `/s` to the Rust API and `/pb` to
PocketBase.
To run pieces directly:
```bash
cd frontend
npm install
npm run dev
```
Export the server's service configuration first:
```bash
export SCREENSHOT_URL=http://localhost:8002
export PUBLIC_URL=http://localhost:3001
export POCKETBASE_URL=http://localhost:8090
export POCKETBASE_ADMIN_EMAIL=...
export POCKETBASE_ADMIN_PASSWORD=...
export GEMINI_API_KEY=...
export GEMINI_MODEL=...
export GOOGLE_MAPS_API_KEY=...
export STRIPE_SECRET_KEY=...
export STRIPE_WEBHOOK_SECRET=...
export STRIPE_REFERRAL_COUPON_ID=...
export GOOGLE_OAUTH_CLIENT_ID=...
export GOOGLE_OAUTH_CLIENT_SECRET=...
# Optional Bugsink/Sentry-compatible error reporting
export BUGSINK_DSN=...
export FRONTEND_BUGSINK_DSN=...
export BUGSINK_ENVIRONMENT=development
export BUGSINK_RELEASE=...
export BUGSINK_SEND_DEFAULT_PII=false
```
```bash
cd server-rs
cargo run -- \
--properties ../property-data/properties.parquet \
--postcode-features ../property-data/postcode.parquet \
--pois ../property-data/filtered_uk_pois.parquet \
--places ../property-data/places.parquet \
--tiles ../property-data/uk.pmtiles \
--postcodes ../property-data/postcode_boundaries \
--travel-times ../property-data/travel-times
```
## Checks
Run the combined local check script:
```bash
./check.sh
```
It runs Python lint/tests, frontend lint/format/typecheck/tests, screenshot
service tests, and Rust clippy/format/tests.
Useful focused commands:
```bash
uv run ruff check .
uv run pytest
cd frontend
npm run lint
npm run typecheck
npm run test
npm run build
cd ../server-rs
cargo clippy --all-targets -- -D warnings
cargo fmt --all --check
cargo test
```
## Production Build
The root `Dockerfile` builds the frontend and Rust server into a runtime image.
Data is mounted at `/app/data`; it is not baked into the image.
```bash
docker build -t property-map .
```
The container entrypoint runs `property-map-server` with the expected data paths
under `/app/data` and serves `frontend/dist` when `--dist` is present.
## Bugsink
Bugsink is wired through the Sentry-compatible SDKs. Set `BUGSINK_DSN` for the
Rust API and `FRONTEND_BUGSINK_DSN` for the browser app. If the frontend DSN is
omitted, the server falls back to `BUGSINK_DSN` when injecting runtime config
into served HTML.
The frontend build also accepts `FRONTEND_BUGSINK_DSN`, `BUGSINK_ENVIRONMENT`,
`BUGSINK_RELEASE`, and `BUGSINK_SEND_DEFAULT_PII` as build-time values. Runtime
HTML injection is preferred for Docker deployments because the DSN can be set
with environment variables when the container starts.
Production Webpack builds emit hidden source maps. Upload them to Bugsink after
building if you want browser stack traces to resolve to source:
```bash
cd frontend
npx sentry-cli sourcemaps inject dist
SENTRY_AUTH_TOKEN=... npx sentry-cli --url https://your-bugsink-instance \
sourcemaps --org bugsinkhasnoorgs --project ignoredfornow upload dist
```