Skip to content

Commit 52a2c0d

Browse files
committed
fix: correct AFDC API pagination — use limit=all instead of broken offset
The AFDC API does not support the offset parameter for pagination. Using limit=all returns all 85,425 stations in a single request. Previous data had only 200 unique stations duplicated 428 times. Rebuilt ev-charging.pmtiles (254KB → 12MB) and ev-charging.json with correct deduplicated data. Top networks: ChargePoint (45,342), Non-Networked (9,267), Blink (6,111), Tesla Destination (5,321), Tesla (3,001)
1 parent e049597 commit 52a2c0d

File tree

3 files changed

+21
-43
lines changed

3 files changed

+21
-43
lines changed

data/ev-charging.json

Lines changed: 1 addition & 1 deletion
Large diffs are not rendered by default.

public/tiles/ev-charging.pmtiles

11.8 MB
Binary file not shown.

scripts/sync-ev-charging.ts

Lines changed: 20 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,6 @@ import * as path from "node:path";
1919

2020
const API_KEY = process.env.NREL_API_KEY ?? "DEMO_KEY";
2121
const BASE_URL = "https://developer.nrel.gov/api/alt-fuel-stations/v1.json";
22-
const LIMIT = 200;
23-
// DEMO_KEY rate limits: ~10 req/hour for IP. Be very conservative.
24-
// With a real NREL_API_KEY you get 1,000 req/hour — reduce this.
25-
const DELAY_MS = API_KEY === "DEMO_KEY" ? 8000 : 1000;
2622

2723
function slugify(str: string): string {
2824
return str
@@ -64,60 +60,42 @@ interface AFDCResponse {
6460
fuel_stations: AFDCStation[];
6561
}
6662

67-
async function fetchPage(offset: number, attempt = 0): Promise<AFDCResponse> {
63+
async function fetchAll(attempt = 0): Promise<AFDCResponse> {
64+
// The AFDC API supports limit=all to return every station in a single request.
65+
// The offset parameter is NOT supported — pagination must be done via limit=all.
6866
const url = new URL(BASE_URL);
6967
url.searchParams.set("api_key", API_KEY);
7068
url.searchParams.set("country", "US");
7169
url.searchParams.set("fuel_type", "ELEC");
72-
url.searchParams.set("limit", String(LIMIT));
73-
url.searchParams.set("offset", String(offset));
70+
url.searchParams.set("limit", "all");
7471

7572
const res = await fetch(url.toString());
7673
if (res.status === 429) {
77-
const waitMs = Math.min(60000 * (attempt + 1), 300000); // 60s, 120s, 180s, max 5min
74+
const waitMs = Math.min(60000 * (attempt + 1), 300000);
7875
console.log(`\n [429 Rate Limited] Waiting ${waitMs / 1000}s before retry (attempt ${attempt + 1})...`);
79-
await sleep(waitMs);
80-
return fetchPage(offset, attempt + 1);
76+
await new Promise((resolve) => setTimeout(resolve, waitMs));
77+
return fetchAll(attempt + 1);
8178
}
8279
if (!res.ok) {
8380
const text = await res.text();
84-
throw new Error(`AFDC API error at offset=${offset}: ${res.status} ${res.statusText}\n${text}`);
81+
throw new Error(`AFDC API error: ${res.status} ${res.statusText}\n${text}`);
8582
}
8683
return res.json() as Promise<AFDCResponse>;
8784
}
8885

89-
function sleep(ms: number): Promise<void> {
90-
return new Promise((resolve) => setTimeout(resolve, ms));
91-
}
92-
9386
async function main() {
9487
console.log(`Syncing EV charging stations from AFDC API (key: ${API_KEY === "DEMO_KEY" ? "DEMO_KEY" : "****"})\n`);
9588

96-
// ── 1. Probe total results ───────────────────────────────────────────────
97-
console.log("1. Probing total results...");
98-
const firstPage = await fetchPage(0);
99-
const totalResults = firstPage.total_results;
100-
console.log(` Total EV stations: ${totalResults}`);
101-
102-
// ── 2. Paginate through all stations ────────────────────────────────────
103-
const allStations: AFDCStation[] = [...firstPage.fuel_stations];
104-
const totalPages = Math.ceil(totalResults / LIMIT);
105-
106-
console.log(`\n2. Fetching ${totalPages} pages (${LIMIT} stations each)...`);
107-
108-
for (let page = 1; page < totalPages; page++) {
109-
const offset = page * LIMIT;
110-
process.stdout.write(` Page ${page + 1}/${totalPages} (offset=${offset})...`);
111-
await sleep(DELAY_MS);
112-
const pageData = await fetchPage(offset);
113-
allStations.push(...pageData.fuel_stations);
114-
process.stdout.write(` ${allStations.length} fetched so far\n`);
115-
}
116-
117-
console.log(`\n Total fetched: ${allStations.length} stations`);
89+
// ── 1. Fetch all stations in a single request ───────────────────────────
90+
// The AFDC API supports limit=all to return every station at once.
91+
console.log("1. Fetching all EV stations (limit=all)...");
92+
const response = await fetchAll();
93+
const allStations = response.fuel_stations;
94+
console.log(` Total results: ${response.total_results}`);
95+
console.log(` Stations received: ${allStations.length}`);
11896

119-
// ── 3. Transform and normalize ───────────────────────────────────────────
120-
console.log("\n3. Transforming station data...");
97+
// ── 2. Transform and normalize ───────────────────────────────────────────
98+
console.log("\n2. Transforming station data...");
12199

122100
const slugsSeen = new Map<string, number>();
123101

@@ -158,11 +136,11 @@ async function main() {
158136
console.log(` Skipped ${skipped} stations with missing coordinates`);
159137
}
160138

161-
// ── 4. Sort by name ────────────────────────────────────────────────────
139+
// ── 3. Sort by name ────────────────────────────────────────────────────
162140
stations.sort((a, b) => a.stationName.localeCompare(b.stationName));
163141

164-
// ── 5. Write output ───────────────────────────────────────────────────
165-
console.log("\n4. Writing output...");
142+
// ── 4. Write output ───────────────────────────────────────────────────
143+
console.log("\n3. Writing output...");
166144
const outPath = path.join(process.cwd(), "data", "ev-charging.json");
167145
fs.writeFileSync(outPath, `${JSON.stringify(stations)}\n`);
168146
const sizeMb = (fs.statSync(outPath).size / 1024 / 1024).toFixed(1);

0 commit comments

Comments
 (0)