Google Maps API costs $7 per 1,000 loads after the free tier. Mapbox charges per tile request. We render 440,000 cafe markers on CoffeeTrove for $0/month using Leaflet + CARTO tiles + a custom clustering system.
The Problem
Rendering 440K markers on a map at once will crash any browser. Marker clustering libraries like Leaflet.markercluster work for 10K points but choke beyond that. We needed a solution that:
- Works at world zoom (showing continents)
- Drills down to individual cafes at street level
- Loads fast on mobile
- Costs nothing
4-Tier Hierarchical System
Instead of client-side clustering, we cluster server-side at 4 zoom levels:
Tier 1: Continents (zoom < 4)
Hardcoded continent centroids with aggregated counts:
const CONTINENT_MAP = {
'Europe': { lat: 48.5, lng: 10, count: 185000 },
'North America': { lat: 42, lng: -100, count: 120000 },
'Asia': { lat: 30, lng: 105, count: 85000 },
// ...
};
7 bubbles on screen. Instant render.
Tier 2: Countries (zoom 4-6)
SELECT country, AVG(lat) as lat, AVG(lng) as lng, COUNT(*) as count
FROM cafes
GROUP BY country;
~120 bubbles. Still instant.
Tier 3: City Grid Clustering (zoom 7-11)
This is where it gets interesting. We use spatial grid merging:
SELECT
ROUND(lat / :grid_size) * :grid_size as grid_lat,
ROUND(lng / :grid_size) * :grid_size as grid_lng,
COUNT(*) as count,
MIN(name) as sample_name
FROM cafes
WHERE lat BETWEEN :south AND :north
AND lng BETWEEN :west AND :east
GROUP BY grid_lat, grid_lng;
Grid size varies by zoom: 0.5 degrees at zoom 7, down to 0.02 at zoom 11. PostgreSQL handles the aggregation in ~50ms for any viewport.
Tier 4: Individual Pins (zoom 12+)
At street level, show actual cafes. But cap at 18 per viewport:
SELECT name, lat, lng, score, chain_type
FROM cafes
WHERE lat BETWEEN :south AND :north
AND lng BETWEEN :west AND :east
ORDER BY score DESC
LIMIT 18;
Airbnb-style cards with name, score badge, and chain indicator.
The Stack (Zero API Keys)
| Component | Cost | Alternative |
|---|---|---|
| Leaflet | Free | Google Maps ($7/1K) |
CARTO nolabels tiles |
Free | Mapbox ($0.25/1K) |
| leaflet.heat (heatmap) | Free | deck.gl |
| PostgreSQL spatial queries | Free | Elasticsearch ($$$) |
Total: $0/month regardless of traffic.
IP-Based Centering
No geolocation popup on load. We read Vercel's x-vercel-ip-latitude and x-vercel-ip-longitude headers to center the map on the user's approximate location. The "Locate me" button triggers precise GPS only on explicit click.
// Server component reads headers
const lat = parseFloat(headers.get('x-vercel-ip-latitude') || '40.7');
const lng = parseFloat(headers.get('x-vercel-ip-longitude') || '-74.0');
No permission popup. No API key. Works on first load.
Performance
- Initial load: 180ms (7 continent bubbles)
- Country zoom: 220ms (120 country bubbles)
- City zoom: 50-80ms (grid query + render)
- Street zoom: 30ms (18 individual pins)
Tested on a $200 Android phone over 4G. Smooth at every zoom level.
Try It
The live map is at coffeetrove.com/map. Zoom from world view down to your street. All 440K cafes, zero API costs.
We use similar spatial techniques on DropThe for city comparison pages, and the same Leaflet stack will power Facil.guide's local tech support finder.
CoffeeTrove is open-source coffee discovery. Built with Next.js 16, PostgreSQL, and Leaflet. No paid map APIs.
Top comments (0)