# 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=...
```

```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.
