Some checks failed
CI / Lint (pull_request) Has been cancelled
CI / Schema map up to date (pull_request) Has been cancelled
CI / Forbidden patterns (9 checks) (pull_request) Has been cancelled
CI / Migrations apply (node-pg-migrate) (pull_request) Has been cancelled
Visual diff / Should run? (pull_request) Has been cancelled
CI / Unit tests (vitest) (pull_request) Has been cancelled
Convoy metrics gate / Require role-event telemetry on convoy PRs (pull_request) Has been cancelled
PR Health rollup / Aggregate gate status (pull_request) Has been cancelled
Preview smoke / Should run? (pull_request) Has been cancelled
Preview smoke / Playwright smoke (pull_request) Has been cancelled
Visual diff / Screenshot diff (pull_request) Has been cancelled
Add pricing schema, free API integrations (MTG bulk, TCGdex, Foolio), valuation and sort-by-price, card detail pricing UI, and nearby store discovery via home zip and OSM geo clients. Co-authored-by: Cursor <cursoragent@cursor.com>
69 lines
1.9 KiB
JavaScript
69 lines
1.9 KiB
JavaScript
const DEFAULT_NOMINATIM_BASE = 'https://nominatim.openstreetmap.org';
|
|
const MIN_REQUEST_INTERVAL_MS = 1000;
|
|
|
|
const ALLOWED_HOSTS = new Set(['nominatim.openstreetmap.org']);
|
|
|
|
let lastRequestAt = 0;
|
|
|
|
function assertAllowedHost(url) {
|
|
const host = new URL(url).hostname.toLowerCase();
|
|
if (!ALLOWED_HOSTS.has(host)) {
|
|
throw new Error(`[nominatim] Host not allowlisted: ${host}`);
|
|
}
|
|
}
|
|
|
|
function getUserAgent() {
|
|
return process.env.NOMINATIM_USER_AGENT ?? 'DeckHearth/1.0';
|
|
}
|
|
|
|
async function throttle() {
|
|
const now = Date.now();
|
|
const elapsed = now - lastRequestAt;
|
|
if (elapsed < MIN_REQUEST_INTERVAL_MS) {
|
|
await new Promise((resolve) => setTimeout(resolve, MIN_REQUEST_INTERVAL_MS - elapsed));
|
|
}
|
|
lastRequestAt = Date.now();
|
|
}
|
|
|
|
/**
|
|
* @param {string} zip Five-digit US zip code.
|
|
* @param {{ fetchImpl?: typeof fetch }} [options]
|
|
* @returns {Promise<{ lat: number, lng: number, displayName: string } | null>}
|
|
*/
|
|
export async function geocodeUsZip(zip, { fetchImpl = fetch } = {}) {
|
|
const base = process.env.NOMINATIM_BASE_URL ?? DEFAULT_NOMINATIM_BASE;
|
|
const url = new URL('/search', base);
|
|
url.searchParams.set('postalcode', zip);
|
|
url.searchParams.set('country', 'US');
|
|
url.searchParams.set('format', 'json');
|
|
url.searchParams.set('limit', '1');
|
|
|
|
assertAllowedHost(url.toString());
|
|
await throttle();
|
|
|
|
const response = await fetchImpl(url.toString(), {
|
|
headers: { 'User-Agent': getUserAgent() },
|
|
});
|
|
|
|
if (!response.ok) {
|
|
throw new Error(`[nominatim] HTTP ${response.status}`);
|
|
}
|
|
|
|
const results = await response.json();
|
|
if (!Array.isArray(results) || results.length === 0) {
|
|
return null;
|
|
}
|
|
|
|
const hit = results[0];
|
|
const lat = Number(hit.lat);
|
|
const lng = Number(hit.lon);
|
|
if (!Number.isFinite(lat) || !Number.isFinite(lng)) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
lat,
|
|
lng,
|
|
displayName: hit.display_name ?? `${zip}, US`,
|
|
};
|
|
}
|