ui phase 9
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
import { NESHAN_WEB_KEY } from '@/config';
|
||||
import type { LatLng } from '@/services/addresses/types';
|
||||
|
||||
/**
|
||||
* Thin client for Neshan's public Search + Reverse-geocoding REST APIs (`api.neshan.org`) — the
|
||||
* address map's search box and reverse-geocoded pin preview (ui-phase-9). This is a **direct
|
||||
* third-party call, not our backend**: `clientFetch` is deliberately NOT used here, since it would
|
||||
* prepend `NEXT_PUBLIC_API_URL` and attach our own bearer token to a third-party request (a
|
||||
* credential leak) — a plain `fetch` with Neshan's own `Api-Key` header is the correct boundary.
|
||||
*
|
||||
* Endpoint paths/headers are confirmed against Neshan's public documentation; the exact response
|
||||
* field names below (`items[].location.x/y`, `formatted_address`) were **not live-verified** in
|
||||
* this sandbox (no network route to platform.neshan.org) — every read here is defensive
|
||||
* (optional-chained, wrapped in try/catch) so a shape drift degrades to "no results" rather than a
|
||||
* crash. Re-verify against platform.neshan.org if search/reverse silently return nothing with a
|
||||
* real key configured.
|
||||
*/
|
||||
|
||||
const SEARCH_URL = 'https://api.neshan.org/v1/search';
|
||||
const REVERSE_URL = 'https://api.neshan.org/v5/reverse';
|
||||
|
||||
export interface NeshanSearchResult {
|
||||
title: string;
|
||||
address: string;
|
||||
location: LatLng;
|
||||
}
|
||||
|
||||
async function neshanGet(url: string): Promise<unknown> {
|
||||
if (!NESHAN_WEB_KEY) return null;
|
||||
try {
|
||||
const response = await fetch(url, { headers: { 'Api-Key': NESHAN_WEB_KEY } });
|
||||
if (!response.ok) return null;
|
||||
return await response.json();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Free-text address search near a point (the map's current center) — `[]` on any failure. */
|
||||
export async function searchNeshan(term: string, near: LatLng): Promise<NeshanSearchResult[]> {
|
||||
const trimmed = term.trim();
|
||||
if (!trimmed) return [];
|
||||
const url = `${SEARCH_URL}?term=${encodeURIComponent(trimmed)}&lat=${near.latitude}&lng=${near.longitude}`;
|
||||
const data = (await neshanGet(url)) as { items?: unknown[] } | null;
|
||||
if (!Array.isArray(data?.items)) return [];
|
||||
|
||||
const results: NeshanSearchResult[] = [];
|
||||
for (const raw of data.items) {
|
||||
const item = raw as { title?: string; address?: string; location?: { x?: number; y?: number } };
|
||||
if (typeof item.location?.x !== 'number' || typeof item.location?.y !== 'number') continue;
|
||||
results.push({
|
||||
title: item.title ?? '',
|
||||
address: item.address ?? '',
|
||||
location: { latitude: item.location.y, longitude: item.location.x },
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/** Reverse-geocodes a pin to a human-readable address — `null` on any failure/unset key. */
|
||||
export async function reverseGeocodeNeshan(point: LatLng): Promise<string | null> {
|
||||
const url = `${REVERSE_URL}?lat=${point.latitude}&lng=${point.longitude}`;
|
||||
const data = (await neshanGet(url)) as { formatted_address?: string } | null;
|
||||
return typeof data?.formatted_address === 'string' && data.formatted_address.length > 0 ? data.formatted_address : null;
|
||||
}
|
||||
Reference in New Issue
Block a user