Skip to content

feat: US EV charging coverage via OpenChargeMap with bbox tiling#86

Merged
GeiserX merged 3 commits into
mainfrom
feat/us-ev-ocm-tiling
Jul 15, 2026
Merged

feat: US EV charging coverage via OpenChargeMap with bbox tiling#86
GeiserX merged 3 commits into
mainfrom
feat/us-ev-ocm-tiling

Conversation

@GeiserX

@GeiserX GeiserX commented Jul 15, 2026

Copy link
Copy Markdown
Owner

What

Adds US EV charging coverage via the existing OpenChargeMap scraper (EV_US), and fixes the silent truncation that made large countries impossible: the scraper issued a single request capped at maxresults=5000, quietly dropping everything beyond it. Germany, GB, France and the Netherlands are already being truncated by this today; the US (~50k+ operational POIs) was unservable.

How

  • Quadtree bbox tiling (src/scrapers/ocm.ts): when the root country query returns at the cap, split the bounding box into 4 and refetch each tile (still countrycode-filtered), recursing on tiles that hit the cap again. BFS order so the request budget refines coverage evenly.
    • Dedupe by POI ID (tiles share edges).
    • Hard limits: MAX_REQUESTS=120 per run, MAX_TILE_DEPTH=9, 300 ms politeness delay between tile requests (PUMPERLY_OCM_TILE_DELAY_MS, tests set 0).
    • Capped pages are still merged — budget exhaustion degrades to partial-but-kept data with an explicit coverage may be partial warning, never silent loss.
    • Small countries keep the exact old behavior: one request, no bbox.
  • EV_US registration (src/instrumentation.ts), 24 h interval like every other EV scraper.
  • Gating fix: PUMPERLY_ENABLED_COUNTRIES derived EV_XX only from tokens that matched a fuel scraper, so a bare US (EV-only country) could never enable EV_US. EV codes now derive from the raw token list.
  • Tests: single-request fast path (asserts no boundingbox param), quadrant tiling with cross-tile dedupe, and request-budget termination on pathological always-capped responses.
  • Docs: README coverage row + .env.example note (US is EV-only — no national fuel-price API exists).

Validation

  • boundingbox=(lat1,lon1),(lat2,lon2) format verified against the live OCM API: a Portland-box query returns only in-box POIs (lat 45.54–45.60, lon −122.94…−123.0), while the same query without bbox returns arbitrary US POIs (OK/CA). Fail-safe by construction: even a regressed bbox filter would only re-fetch country-wide pages, which dedupe collapses — coverage falls back to today's behavior, never worse.
  • Unit tests cover the tiling logic; CI runs lint/typecheck/tests/build.

Scope notes

  • This makes US chargers browsable on the map (station type ev_charger, connector metadata from OCM). US route planning is not included — prod Valhalla/Photon only carry European extracts; that's a separate infra decision.
  • Deployment note (prod): PUMPERLY_ENABLED_COUNTRIES is pinned in the compose env — needs ,US appended for pumperly.com to pick this up after release.

Closes #85. Refs #78 (community static-data path remains the fallback for regions OCM covers poorly).

Summary by CodeRabbit

  • New Features

    • Added United States coverage for electric vehicle charging stations.
    • Expanded charging-station retrieval to cover more locations when results exceed service limits.
    • Added safeguards to avoid duplicate stations and identify potentially partial results.
  • Documentation

    • Clarified coverage details for fuel prices and U.S. EV charging stations.
    • Documented that U.S. coverage is limited to EV chargers and does not include national fuel-price data.

The OCM scraper issued a single request capped at 5000 results, silently
truncating any country above the cap (DE/GB/FR/NL are affected today).
Register EV_US and fix the truncation: when the root query returns at the
cap, quadtree-tile the bounding box (still countrycode-filtered), recurse
on capped tiles, dedupe by POI ID, all under a hard request budget with a
politeness delay.

- ocm.ts: BFS bbox tiling (MAX_REQUESTS=120, MAX_TILE_DEPTH=9, 300ms delay
  via PUMPERLY_OCM_TILE_DELAY_MS), per-run request/POI logging, partial-
  coverage warning instead of silent truncation
- instrumentation.ts: EV_US registration (24h interval like other EV
  scrapers); enabled-countries now derives EV_XX from raw tokens so a bare
  "US" (EV-only country, no fuel scraper) enables EV_US
- tests: single-request fast path, quadrant tiling + cross-tile dedupe,
  request-budget termination on pathological capped responses
- README/.env.example: document US EV-only coverage

bbox param format validated against the live OCM API (Portland box returns
only in-box POIs; control query without bbox returns arbitrary US POIs).

Closes #85. Refs #78.
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@GeiserX, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 38 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 140adb71-3c20-4269-afcc-d083fa7466ad

📥 Commits

Reviewing files that changed from the base of the PR and between 9f3fc3c and 17b8eb3.

📒 Files selected for processing (3)
  • src/scrapers/cli.ts
  • src/scrapers/ocm.test.ts
  • src/scrapers/ocm.ts
📝 Walkthrough

Walkthrough

Changes

US EV coverage

Layer / File(s) Summary
Register and enable US EV scraping
src/instrumentation.ts
Registers EV_US with a 24-hour interval, creates an OCMScraper("US"), and derives EV codes from raw country tokens.
Document US EV-only coverage
.env.example, README.md
Documents US EV-charger coverage and the absence of a national US fuel-price API.

Open Charge Map tiling

Layer / File(s) Summary
Bounded tile request primitives
src/scrapers/ocm.ts
Adds tile depth, request budget, delay controls, bounding-box request construction, and HTTP error handling.
Expand capped country queries
src/scrapers/ocm.ts
Splits capped root results into tiles, deduplicates POIs, respects limits, warns on partial coverage, and logs updated metrics.
Validate capped-result behavior
src/scrapers/ocm.test.ts
Tests uncapped requests, quadrant tiling, country scoping, deduplication, request budgets, and partial warnings.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

  • GeiserX/Pumperly#79: Both changes extend instrumentation registration and country enablement for specialized scraper keys.
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: US EV charging coverage via OpenChargeMap and bbox tiling.
Linked Issues check ✅ Passed The PR delivers US EV charging coverage and improved OCM handling, which covers the Portland/Beaverton EV data goal.
Out of Scope Changes check ✅ Passed The docs, tests, and instrumentation changes all support the EV_US coverage and tiling work; no unrelated changes stand out.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/us-ev-ocm-tiling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/scrapers/ocm.test.ts (1)

223-240: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a maximum-depth termination test.

This only exercises request-budget exhaustion. Add a case where one branch remains capped through depth 9 while sibling tiles return empty, then assert termination and the partial-coverage warning. That protects the distinct Lines 166-170 depth-limit path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/scrapers/ocm.test.ts` around lines 223 - 240, Add a test alongside the
existing request-budget test that mocks one tile branch to remain capped through
depth 9 while sibling tile requests return empty responses. Assert that
scraper.fetch() terminates and emits the partial-coverage warning, specifically
covering the maximum-depth handling in OCMScraper rather than request-budget
exhaustion.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/scrapers/ocm.ts`:
- Line 23: Update the scraper loop around MAX_REQUESTS and the request handling
near the referenced request block to enforce a total run deadline, limiting each
request timeout to the remaining duration rather than a fixed 120 seconds. When
the deadline expires, stop issuing requests, preserve the POIs accumulated so
far, and emit a warning indicating that coverage is partial due to the time
limit.
- Line 25: Validate the value assigned to TILE_DELAY_MS after parsing
PUMPERLY_OCM_TILE_DELAY_MS, falling back to 300 ms when it is NaN, infinite, or
negative. Preserve configured finite, non-negative values so throttling remains
enabled as intended.
- Line 126: Update fetchPage around the res.json() return to parse the response
through the existing or appropriate Zod schema before returning it. Ensure
malformed OCM POI data is rejected at this boundary, while valid parsed data
continues into the tiling and deduping flow.

---

Nitpick comments:
In `@src/scrapers/ocm.test.ts`:
- Around line 223-240: Add a test alongside the existing request-budget test
that mocks one tile branch to remain capped through depth 9 while sibling tile
requests return empty responses. Assert that scraper.fetch() terminates and
emits the partial-coverage warning, specifically covering the maximum-depth
handling in OCMScraper rather than request-budget exhaustion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 62ccccc4-92ae-40f0-9a31-089b52acd96a

📥 Commits

Reviewing files that changed from the base of the PR and between f13ef8f and 9f3fc3c.

📒 Files selected for processing (5)
  • .env.example
  • README.md
  • src/instrumentation.ts
  • src/scrapers/ocm.test.ts
  • src/scrapers/ocm.ts

Comment thread src/scrapers/ocm.ts
Comment thread src/scrapers/ocm.ts Outdated
Comment thread src/scrapers/ocm.ts Outdated
GeiserX added 2 commits July 15, 2026 12:06
cli.test.ts guards that instrumentation and the manual-scrape CLI expose
the identical EV_* key set; EV_US was added to instrumentation only.
…Zod boundary validation

- Bound the tiling loop by elapsed time (15min) as well as request count;
  120 slow responses could otherwise stall a scrape cycle for hours.
- Sanitize PUMPERLY_OCM_TILE_DELAY_MS (NaN/negative falls back to 300ms
  instead of silently disabling throttling).
- Validate the OCM payload with a minimal Zod schema at the response
  boundary: reject non-array payloads with a clear error, drop malformed
  POIs with a warning; replaces the hand-written interfaces.
@GeiserX

GeiserX commented Jul 15, 2026

Copy link
Copy Markdown
Owner Author

All three findings addressed in the latest commit — thanks @coderabbitai:

  1. Elapsed-time bound: tiling loop now also stops at a 15-minute time budget (not just the 120-request budget), so slow OCM responses can't stall a scrape cycle for hours.
  2. Delay validation: PUMPERLY_OCM_TILE_DELAY_MS is sanitized — NaN/negative values fall back to the 300 ms default instead of silently disabling throttling.
  3. Boundary validation: OCM payloads now parse through a minimal Zod schema — non-array payloads throw a clear error, malformed POIs are dropped with a warning (replaces the hand-written interfaces). Both behaviors covered by new tests.

@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
fd ocm.ts src/scrapers
echo "---"
rg -n "MAX_ELAPSED|TILE_DELAY_MS|z\.|zod|safeParse|parse\(" src/scrapers/ocm.ts

Length of output: 1136


🏁 Script executed:

#!/bin/bash
rg -n "Date.now|startTime|elapsed|MAX_.*MS|MAX_.*TIME|15 \* 60|900000" src/scrapers/ocm.ts

Length of output: 717


@GeiserX Confirmed all three in src/scrapers/ocm.ts:

  1. Elapsed-time bound: MAX_SCRAPE_MS = 15 * 60 * 1000 (Line 25) checked alongside MAX_REQUESTS in the loop condition at Line 165, with startedAt captured at Line 143.
  2. Delay validation: Lines 29-30 sanitize PUMPERLY_OCM_TILE_DELAY_MS, falling back to 300ms on NaN/negative.
  3. Boundary validation: OCMPOISchema (Zod, Lines 57-73) with safeParse at Line 124 replacing the hand-written interfaces.

Looks good — thanks for addressing these.

🐇✨

@GeiserX
GeiserX merged commit aca2a84 into main Jul 15, 2026
8 checks passed
@GeiserX
GeiserX deleted the feat/us-ev-ocm-tiling branch July 15, 2026 10:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add Portland / Beaverton, OR EV Charging Station Data for Pacific Northwest Region

1 participant