fix: fix osm2pgsql categories, remove unused parameter, increase parallellity in transit, update readme

This commit is contained in:
Jan-Henrik 2026-03-06 13:46:44 +01:00
parent fe6e59f427
commit f067472fd5
4 changed files with 80 additions and 22 deletions

View file

@ -15,9 +15,12 @@ Next.js App Server
├── PostgreSQL + PostGIS (POIs, grid points, precomputed scores)
└── Valkey (API response cache, BullMQ queues)
BullMQ Worker (download queue, concurrency 1)
└── download-pbf → streams OSM PBF from Geofabrik (serialised to avoid
redundant parallel downloads; idempotent if file exists)
BullMQ Worker (pipeline queue, concurrency 8)
├── refresh-city → orchestrates full ingest via FlowProducer
├── download-pbf → streams OSM PBF from Geofabrik
├── extract-pois → osmium filter + osm2pgsql flex → raw_pois
├── generate-grid → PostGIS 200 m hex grid → grid_points
├── compute-scores → two-phase orchestrator (see Scoring below)
@ -108,13 +111,73 @@ docker compose up postgres valkey -d
## Category Definitions
| Category | OSM Sources |
|----------|-------------|
| Service & Trade | supermarkets, shops, restaurants, pharmacies, banks, cafés |
| Transport | bus stops, metro, tram, train stations, bike share, car share |
| Work & School | offices, coworking, schools, kindergartens, universities |
| Culture & Community | libraries, hospitals, clinics, museums, theatres, community centres |
| Recreation | parks, playgrounds, sports centres, gyms, green spaces |
Five categories cover everyday destinations. All categories are scored at the same threshold (5, 10, 15, 20, or 30 minutes — user-selectable). The **universal weight** column shows how strongly a subcategory contributes to its parent category score in the Universal profile (range 01); other profiles override specific values — see the Profiles table below.
### Service & Trade
| OSM tag(s) | Subcategory | Universal weight |
|---|---|:---:|
| `shop=supermarket`, `shop=wholesale` | `supermarket` | 1.0 |
| `shop=convenience` | `convenience` | 0.65 |
| `amenity=pharmacy`, `shop=pharmacy` | `pharmacy` | 1.0 |
| `amenity=restaurant`, `amenity=fast_food` | `restaurant` | 0.55 |
| `amenity=cafe`, `shop=bakery` | `cafe` | 0.4 |
| `amenity=bank` | `bank` | 0.35 |
| `amenity=post_office` | `post_office` | 0.4 |
| `shop=greengrocer`, `shop=butcher`, `amenity=marketplace`, `shop=department_store`, `shop=mall` | `market` | 0.4 |
| `shop=laundry`, `shop=dry_cleaning` | `laundry` | 0.3 |
| `amenity=atm` | `atm` | 0.2 |
### Transport
| OSM tag(s) | Subcategory | Universal weight |
|---|---|:---:|
| `railway=station`, `railway=halt` | `train_station` | 1.0 |
| `railway=subway_entrance`, `railway=subway_station` | `metro` | 1.0 |
| `railway=tram_stop` | `tram_stop` | 0.75 |
| `highway=bus_stop`, `amenity=bus_station` | `bus_stop` | 0.55 |
| `amenity=ferry_terminal` | `ferry` | 0.5 |
| `amenity=bicycle_rental` | `bike_share` | 0.5 |
| `amenity=car_sharing` | `car_share` | 0.5 |
**Excluded:** `public_transport=stop_position` and `public_transport=platform` — these duplicate `highway=bus_stop` / `railway=tram_stop` nodes at the exact same location and would double-count stops in scoring.
### Work & School
| OSM tag(s) | Subcategory | Universal weight |
|---|---|:---:|
| `amenity=kindergarten`, `amenity=childcare` | `kindergarten` | 0.75 |
| `amenity=school` | `school` | 0.7 |
| `office=coworking` | `coworking` | 0.55 |
| `amenity=university`, `amenity=college` | `university` | 0.55 |
| `amenity=driving_school` | `driving_school` | 0.2 |
**Excluded:** `office=company`, `office=government`, `landuse=commercial`, `landuse=office` — ubiquitous in every urban block; they always win the "nearest POI" race in the detail view, masking meaningful destinations. Government buildings are captured via `amenity=townhall` / `amenity=police` in Culture & Community instead.
### Culture & Community
| OSM tag(s) | Subcategory | Universal weight |
|---|---|:---:|
| `amenity=hospital` | `hospital` | 1.0 |
| `amenity=clinic`, `amenity=doctors` | `clinic` | 0.8 |
| `amenity=library` | `library` | 0.7 |
| `amenity=community_centre`, `leisure=arts_centre` | `community_center` | 0.6 |
| `amenity=social_facility` | `social_services` | 0.6 |
| `amenity=theatre`, `amenity=cinema` | `theatre` | 0.5 |
| `tourism=museum` | `museum` | 0.4 |
| `amenity=townhall`, `amenity=police` | `government` | 0.4 |
| `amenity=place_of_worship` | `place_of_worship` | 0.25 |
### Recreation
| OSM tag(s) | Subcategory | Universal weight |
|---|---|:---:|
| `leisure=park`, `leisure=garden` | `park` | 1.0 |
| `leisure=playground` | `playground` | 0.85 |
| `leisure=sports_centre`, `leisure=pitch` | `sports_facility` | 0.65 |
| `leisure=fitness_centre` | `gym` | 0.65 |
| `leisure=nature_reserve`, `leisure=golf_course`, `landuse=recreation_ground`, `landuse=grass`, `landuse=meadow`, `landuse=forest` | `green_space` | 0.6 |
| `leisure=swimming_pool`, `amenity=swimming_pool` | `swimming_pool` | 0.55 |
## Scoring
@ -174,13 +237,13 @@ Each profile carries two sets of weights:
- **Category weights** (used as slider presets in the UI, range 02): how much relative importance each of the 5 categories receives in the composite score.
- **Subcategory weights** (baked into precomputed scores, range 01): how strongly a specific subcategory contributes to its parent category score.
| Profile | Emoji | Category emphasis | Notable subcategory boosts |
|---------|-------|-------------------|---------------------------|
| Universal | ⚖️ | All equal (1.0) | Balanced baseline |
| Young Family | 👨‍👩‍👧 | Work & School 1.5×, Recreation 1.4×, Service 1.2× | school, kindergarten, playground, clinic all → 1.0 |
| Senior | 🧓 | Culture & Community 1.5×, Service 1.4×, Transport 1.1× | hospital, clinic, pharmacy, social services → 1.0; school → 0.05 |
| Young Professional | 💼 | Transport 1.5×, Recreation 1.1× | metro, train → 1.0; gym 0.9; coworking 0.85; school → 0.1 |
| Student | 🎓 | Work & School 1.5×, Transport 1.4×, Culture 1.2× | university, library → 1.0; bike share 0.85; school → 0.05 |
| Profile | Emoji | Category weights | Notable subcategory overrides (vs. Universal) |
|---------|-------|------------------|------------------------------------------------|
| Universal | ⚖️ | All 1.0 | Baseline — see tables above for all weights |
| Young Family | 👨‍👩‍👧 | Work & School 1.5, Recreation 1.4, Service 1.2, Culture 0.9, Transport 1.0 | school → 1.0, kindergarten → 1.0, playground → 1.0, clinic → 1.0, park → 1.0; gym → 0.5, university → 0.2, driving\_school → (inherits 0.2) |
| Senior | 🧓 | Culture & Community 1.5, Service 1.4, Transport 1.1, Recreation 1.0, Work & School 0.3 | hospital → 1.0, clinic → 1.0, pharmacy → 1.0, social\_services → 0.9, bus\_stop → 0.75, tram\_stop → 0.8, metro → 0.8; school → 0.05, kindergarten → 0.05, university → 0.15 |
| Young Professional | 💼 | Transport 1.5, Recreation 1.1, Service 1.0, Culture 0.9, Work & School 0.7 | metro → 1.0, train\_station → 1.0, tram\_stop → 0.85, bike\_share → 0.7; gym → 0.9, restaurant → 0.75, coworking → 0.85; school → 0.1, kindergarten → 0.05 |
| Student | 🎓 | Work & School 1.5, Transport 1.4, Culture & Community 1.2, Service 0.9, Recreation 0.8 | university → 1.0, library → 1.0, coworking → 0.9, bike\_share → 0.85, cafe → 0.9, metro → 1.0, train\_station → 0.9; school → 0.05, kindergarten → 0.05 |
### Composite score

View file

@ -99,6 +99,7 @@ local tag_map = {
swimming_pool = { 'recreation', 'swimming_pool' },
garden = { 'recreation', 'park' },
nature_reserve = { 'recreation', 'green_space' },
golf_course = { 'recreation', 'green_space' },
pitch = { 'recreation', 'sports_facility' },
arts_centre = { 'culture_community', 'community_center' },
},

View file

@ -22,7 +22,6 @@ export interface CategoryDefinition {
id: CategoryId;
label: string;
defaultWeight: number;
defaultThresholdMinutes: number;
color: string;
tags: TagFilter[];
}
@ -32,7 +31,6 @@ export const CATEGORIES: CategoryDefinition[] = [
id: "service_trade",
label: "Service & Trade",
defaultWeight: 0.2,
defaultThresholdMinutes: 10,
color: "#e63946",
tags: [
{
@ -70,7 +68,6 @@ export const CATEGORIES: CategoryDefinition[] = [
id: "transport",
label: "Transport",
defaultWeight: 0.2,
defaultThresholdMinutes: 8,
color: "#457b9d",
tags: [
{ key: "highway", values: ["bus_stop"] },
@ -92,7 +89,6 @@ export const CATEGORIES: CategoryDefinition[] = [
id: "work_school",
label: "Work & School",
defaultWeight: 0.2,
defaultThresholdMinutes: 20,
color: "#2a9d8f",
tags: [
{ key: "office", values: ["coworking"] },
@ -113,7 +109,6 @@ export const CATEGORIES: CategoryDefinition[] = [
id: "culture_community",
label: "Culture & Community",
defaultWeight: 0.2,
defaultThresholdMinutes: 15,
color: "#e9c46a",
tags: [
{
@ -140,7 +135,6 @@ export const CATEGORIES: CategoryDefinition[] = [
id: "recreation",
label: "Recreation",
defaultWeight: 0.2,
defaultThresholdMinutes: 10,
color: "#06d6a0",
tags: [
{

View file

@ -30,7 +30,7 @@ export type ComputeTransitData = {
};
/** Grid points processed per concurrent Valhalla isochrone call. */
const BATCH_CONCURRENCY = 4;
const BATCH_CONCURRENCY = 8;
/** Rows per INSERT. */
const INSERT_CHUNK = 2000;