- Replace 200+ individual mapboxgl.Marker DOM nodes with a single GeoJSON
source using Mapbox built-in clustering (clusterRadius=50, maxZoom=14)
- Cluster + unclustered price labels render as WebGL symbol/circle layers —
zero per-frame DOM cost, 60fps pan on mid-range Android
- Decouple selectedListingId updates from full marker teardown: selection
state is now a `selected:0|1` feature property, updated via setData() only
- fitBounds no longer fires on hover/selection changes — camera moves only
when the listings array identity changes (filter change)
- Fix stale onMarkerClick closure with a stable ref pattern
- Decided clustering strategy: Mapbox built-in over supercluster (no extra
dep, sufficient for <5k results; see docs/perf/listing-map-perf-analysis.md)
- Add perf analysis doc to apps/web/docs/perf/
Co-Authored-By: Paperclip <noreply@paperclip.ing>