⚡ Autodun EV Finder
Real-time UK EV charging intelligence — map, score, and analyse 30,000+ charge points with machine learning
Autodun EV Finder is a production Next.js application that aggregates live UK electric vehicle charging data from OpenChargeMap, overlays UK local authority boundaries, collects real driver feedback, and runs a nightly machine learning pipeline to score every station for reliability and suitability.
It ships three interfaces:
| Interface | Path | Purpose |
|---|---|---|
| Public map | / |
Interactive Leaflet map with heatmap, marker clustering, postcode search, AI score drawer |
| Council dashboard | /ev-charging-council-dashboard |
Marketing page for UK local authorities — explains the data product |
| Admin dashboard | /admin/feedback |
Protected analytics — feedback trends, ML metrics, CSV export, council overlay |
The map renders 30,000+ charge points across the UK using React-Leaflet with react-leaflet-cluster for performant marker clustering (maxClusterRadius: 60, unclusters at zoom 16). Three toggleable layers:
- Markers — individual station pins, click to open the detail drawer
- Heatmap —
leaflet.heatdensity overlay with adaptive radius based on zoom level; auto-downsamples above 25k points - Council — purple diamond markers for council-sourced charging stations fetched from
/api/council-stations
Search uses a two-stage geocoder: postcodes.io for UK postcodes, with Nominatim (UK-biased viewport) as fallback.
Every station receives a 0–1 suitability score from a lightweight linear model trained nightly on real feedback data:
score = w·power_kw + w·n_connectors + w·has_fast_dc + w·rating + w·has_geo + bias
The model file (ml/model.json) contains learned weights and normalization caps. The scoring API (/api/score) runs the prediction server-side with:
- 30-minute LRU-TTL cache (800 entries) to avoid recomputation
- IP rate limiting (60 req/min) to prevent abuse
- Optional Supabase persistence for audit trails
- Client-side localStorage cache (30 min) to reduce network calls
Scores render in the station drawer as a circular SVG progress ring with color coding: green (≥75%), amber (50–74%), red (<50%).
A GitHub Actions workflow (.github/workflows/train-ml.yml) runs nightly at 02:30 UTC:
- Pulls recent feedback from Supabase
- Trains a weighted linear model using Python + NumPy
- Logs accuracy, precision, and recall to the
ml_runstable - Commits the updated
ml/model.jsonback tomain
Training metrics are visible at /ml-status (public) and /admin/ml (detailed admin view with accuracy-over-time charts).
Protected admin panel at /admin/feedback with:
- Summary KPI tiles (total / good / bad / today / avg ML score)
- Multi-axis filtering: sentiment, score range, date range, free text, model version, data source
- Timeline bar chart and compact distribution charts (sentiment split, ML score histogram, source breakdown)
- Council overlay map showing feedback marker locations
- Paginated feedback table with full detail
- One-click CSV export for offline analysis
Council data is sourced from Supabase (PostGIS) with boundary polygons and centroids for every UK local authority. API endpoints:
/api/council— lookup council by point coordinates or ID/api/council-stations— return charging stations clipped to a council polygon's bounding box/api/cron/council-refresh— warm endpoint for cron-based cache priming
┌─────────────────────────────────────────────────────────────┐
│ Browser │
│ ┌──────────┐ ┌──────────────┐ ┌────────────────────────┐ │
│ │ index.jsx│ │EnhancedMapV2 │ │ StationDrawer.tsx │ │
│ │ (page) │──│ (Leaflet) │──│ (AI score, feedback) │ │
│ └────┬─────┘ └──────┬───────┘ └──────────┬─────────────┘ │
│ │ │ │ │
│ URL state viewport bbox POST /api/feedback │
│ management triggers fetch POST /api/score │
└───────┼───────────────┼──────────────────────┼───────────────┘
│ │ │
┌───────▼───────────────▼──────────────────────▼───────────────┐
│ Next.js API Routes │
│ │
│ /api/stations ──── OpenChargeMap API (GB, tiled bbox) │
│ /api/council ──── Supabase PostGIS (boundaries, centroids) │
│ /api/score ──── ml/scorer.ts (local linear model) │
│ /api/feedback ──── Supabase (feedback table, insert) │
└──────────────────────────────┬───────────────────────────────┘
│
┌──────────────────────────────▼───────────────────────────────┐
│ Data Layer │
│ │
│ OpenChargeMap API ─── 30k+ GB stations (live, bbox queries) │
│ Supabase/PostGIS ──── feedback, ml_runs, council boundaries │
│ ml/model.json ──────── trained weights (nightly via GH CI) │
│ postcodes.io ─────── UK postcode → lat/lng geocoding │
└──────────────────────────────────────────────────────────────┘
Data flow: The map fires a moveend event (debounced 800ms, gated to zoom ≥ 10). ViewportFetcher computes the viewport bounding box, checks the client-side LRU cache, and calls /api/stations?bbox=...&tiles=N. The API route splits the bbox into tiles, fetches each from OpenChargeMap in parallel, normalizes connector data, and returns GeoJSON. Station features are enriched with AI scores via concurrent /api/score POST calls (3 workers, 25 per batch). Results are cached at both the API layer (s-maxage=300) and client (api-cache.js with getCached/setCache).
- Node.js 20.x
- npm 9+
- An OpenChargeMap API key (free)
- A Supabase project (free tier works)
git clone https://github.com/kamrangul87/autodun-ev-finder.git
cd autodun-ev-finder
npm installCreate a .env.local file in the project root:
# OpenChargeMap
OCM_API_KEY=your_openchargemap_api_key_here
# Supabase
NEXT_PUBLIC_SUPABASE_URL=https://your-project.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your_anon_key_here
SUPABASE_SERVICE_ROLE_KEY=your_service_role_key_here
# AI Scorer (set to "true" to enable ML scoring)
NEXT_PUBLIC_SCORER_ENABLED=true
# Optional: custom tile server
# NEXT_PUBLIC_TILE_URL=https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png# Development (port 5000)
npm run dev
# Production build
npm run build
npm start
# Lint
npm run lintOpen http://localhost:5000 to see the map.
autodun-ev-finder/
├── pages/
│ ├── index.jsx # Main map page
│ ├── ml-status.tsx # Public ML metrics
│ ├── ev-charging-council-dashboard.tsx # Council landing page
│ ├── about-ai.tsx # AI explainer page
│ ├── privacy.tsx # Privacy policy
│ ├── admin/
│ │ ├── feedback.tsx # Admin feedback dashboard
│ │ └── ml.tsx # Admin ML dashboard
│ └── api/
│ ├── stations.js # Station data (OCM + fallbacks)
│ ├── score.ts # AI scoring endpoint
│ ├── feedback.js # Feedback submission
│ ├── council.ts # Council boundary lookup
│ ├── council-stations.ts # Council-scoped stations
│ └── cron/ # Scheduled jobs
├── components/
│ ├── EnhancedMapV2.jsx # Main Leaflet map component
│ ├── StationDrawer.tsx # Station detail panel + AI score ring
│ ├── LocateMeButton.tsx # Geolocation control
│ ├── SearchBox.tsx # Postcode search
│ └── admin/ # Admin dashboard components
├── ml/
│ ├── train.py # Nightly training script (Python)
│ ├── scorer.ts # Server-side model inference
│ ├── model.json # Trained model weights
│ └── training_data.csv # Training dataset
├── lib/
│ ├── data-sources.js # OCM / static / demo data fetchers
│ ├── api-cache.js # Client-side LRU cache
│ ├── postcode-search.js # Geocoding (postcodes.io + Nominatim)
│ ├── supabaseAdmin.ts # Supabase service role client
│ ├── model1.ts # Feature extraction + scoring logic
│ └── viewportScorer.ts # Batched viewport AI scoring
├── utils/
│ ├── haversine.ts # Distance calculations
│ ├── geo.ts # Bbox parsing + UK bounds
│ ├── telemetry.ts # Event tracking
│ └── url-state.js # URL ↔ state sync
├── .github/workflows/
│ ├── ci.yml # Build + lint on push/PR
│ ├── train-ml.yml # Nightly ML training
│ └── ev-ingest.yml # EV data ingestion
└── public/ # Static assets
| Map View | Station Drawer | Admin Dashboard |
|---|---|---|
![]() |
![]() |
![]() |
Screenshots not included in repo yet. Contribute by adding them to
docs/screenshots/.
| Variable | Required | Description |
|---|---|---|
OCM_API_KEY |
Yes | OpenChargeMap API key for live station data |
NEXT_PUBLIC_SUPABASE_URL |
Yes | Supabase project URL |
NEXT_PUBLIC_SUPABASE_ANON_KEY |
Yes | Supabase anonymous/public key |
SUPABASE_SERVICE_ROLE_KEY |
Yes | Supabase service role key (server-side only) |
NEXT_PUBLIC_SCORER_ENABLED |
No | Set to "true" to enable ML scoring (defaults to fallback score) |
NEXT_PUBLIC_TILE_URL |
No | Custom map tile server URL |
The app deploys automatically to Vercel on every push to main. The production instance runs at ev.autodun.com.
# Manual deploy via Vercel CLI
npx vercel --prodEnsure all environment variables are configured in your Vercel project settings.
Contributions are welcome. Please:
- Fork the repository
- Create a feature branch (
git checkout -b feature/your-feature) - Write clear commit messages
- Ensure
npm run buildandnpm run lintpass - Open a pull request against
main
- Improving ML model accuracy with more training features
- Adding connector-type filtering on the map
- Accessibility improvements (WCAG AA compliance)
- Unit and integration test coverage
- Mobile PWA enhancements
This project is licensed under the MIT License.
Built by Autodun Helping UK councils and drivers make smarter EV charging decisions


