Interactive map of Swedish police incident reports (crimes, accidents, disturbances) with drill-down search, category filtering, and population-normalized rates across all 290 municipalities.
🗺️ Explore the map — Live demo
# Install dependencies
make install
# Fetch events data and build pipeline
make fetch-events
make pipeline-all
# Start local server
make serve
# Open http://localhost:8080Prerequisites: Python 3.13+, uv, Tippecanoe
Interactive Map
- Vector tiles rendered with MapLibre GL JS and PMTiles
- Municipality boundaries with incident counts and rates
- Choropleth coloring by absolute count or rate per 10,000 population
Event Drill-Down
- Click any region to browse individual events
- Full-text search with Swedish stemming ("stöld" matches "stölder")
- Filter by 8 crime categories: traffic, property, violence, narcotics, fraud, public order, weapons, other
- Date range presets (7d, 30d, 90d) or custom date picker
- Paginated results with event details and links to police reports
Responsive Design
- Desktop: slide-out drawer for event list
- Mobile: bottom sheet with touch-friendly controls
- Keyboard shortcuts (
Ssearch,Escclose,?help)
crimecity3k/
├── crimecity3k/
│ ├── api/ # FastAPI backend
│ │ ├── main.py # App, routes, static file serving
│ │ ├── queries.py # DuckDB event queries
│ │ ├── fts.py # Full-text search index
│ │ ├── categories.py # Event type → category mapping
│ │ └── schemas.py # Pydantic request/response models
│ ├── municipality_*.py # Municipality data processing
│ └── sql/
│ └── municipality_aggregation.sql
├── static/ # Frontend (no build step)
│ ├── index.html
│ ├── app.js # MapLibre + drill-down UI (~1400 lines)
│ └── style.css
├── tests/
│ ├── test_*.py # pytest unit/integration tests
│ └── test_frontend_e2e.py # Playwright browser tests
├── data/ # Generated data (gitignored)
│ ├── events.parquet # Source events
│ ├── municipalities/ # Aggregated parquet files
│ └── tiles/ # GeoJSONL and PMTiles
└── Makefile # Build orchestration
| Endpoint | Description |
|---|---|
GET /api/events |
Query events by H3 cell or location with filtering |
GET /api/types |
Category → event types hierarchy for filter UI |
GET /health |
Health check with event count |
GET /docs |
Interactive OpenAPI documentation |
Query parameters for /api/events:
h3_cellorlocation_name(required, mutually exclusive)start_date,end_date— date range filtercategories,types— category/type filtersearch— full-text search querypage,per_page— pagination (max 100 per page)
Upstream: polisen-se-events-history (GitHub release)
↓ make fetch-events
events.parquet (100k+ police events, 2022-present)
↓ municipality_aggregation.sql
Municipality aggregates with category counts + type breakdown
↓ GeoJSON export
GeoJSONL (municipalities.geojsonl.gz)
↓ Tippecanoe
PMTiles (municipalities.pmtiles)
↓
Interactive web map
make test # All tests with coverage
make test-unit # Unit tests only (fast)
make test-e2e # Playwright browser tests
make check # Lint (ruff) + type check (mypy)
make format # Auto-format code
make help # All available targetsmake deploy # Build container, upload, and deploy
make deploy-status # Check production status
make deploy-logs # View container logs- Police Incidents: polisen-se-events-history — daily scrapes from polisen.se API (official "händelsenotiser")
- Municipality Boundaries: Swedish Land Survey (Lantmäteriet)
- Population Data: Statistics Sweden (SCB), 2024
Code: Public Domain
Data: Subject to source terms of use
Implemented by @stefanholek and @dnouri.