/*! @navvo/maps-js v0.1.0 | (c) 2026 Navvo | MIT License | https://docs.navvo.io */

// src/core/coords.ts
function toLngLat(c) {
  if (Array.isArray(c)) {
    assertSane(c[0], c[1]);
    return [c[0], c[1]];
  }
  assertSane(c.lng, c.lat);
  return [c.lng, c.lat];
}
function toLatLonObj(c) {
  const [lng, lat] = toLngLat(c);
  return { lat, lon: lng };
}
function toLonLatObj(c, name) {
  const [lng, lat] = toLngLat(c);
  return name !== void 0 ? { lon: lng, lat, name } : { lon: lng, lat };
}
function toLatLngObj(c) {
  const [lng, lat] = toLngLat(c);
  return { lat, lng };
}
function parseWkt(wkt) {
  if (!wkt || typeof wkt !== "string") return null;
  const m = /POINT\s*\(\s*([-\d.eE]+)\s+([-\d.eE]+)\s*\)/i.exec(wkt);
  if (!m) return null;
  const lng = Number(m[1]);
  const lat = Number(m[2]);
  if (!Number.isFinite(lng) || !Number.isFinite(lat)) return null;
  return [lng, lat];
}
function bboxParam(b) {
  if (typeof b === "string") return b;
  if (Array.isArray(b)) {
    const [[w, s], [e, n]] = b;
    return `${w},${s},${e},${n}`;
  }
  return `${b.getWest()},${b.getSouth()},${b.getEast()},${b.getNorth()}`;
}
function latLonParams(c) {
  const [lng, lat] = toLngLat(c);
  return { lat: String(lat), lon: String(lng) };
}
function num(v) {
  if (typeof v === "number") return Number.isFinite(v) ? v : null;
  if (typeof v === "string" && v.trim() !== "") {
    const n = Number(v);
    return Number.isFinite(n) ? n : null;
  }
  return null;
}
function assertSane(lng, lat) {
  if (typeof lat === "number" && Math.abs(lat) > 90) {
    if (typeof console !== "undefined") {
      console.warn(
        `[NavvoMaps] Suspicious coordinate: latitude ${lat} is out of [-90, 90]. Did you pass [lat, lng] instead of [lng, lat]?`
      );
    }
  }
}

// src/core/errors.ts
var NavvoError = class extends Error {
  constructor(message, status = 0, code = "error", opts) {
    super(message);
    this.name = "NavvoError";
    this.status = status;
    this.code = code;
    this.path = opts?.path;
    this.body = opts?.body;
    Object.setPrototypeOf(this, new.target.prototype);
  }
};
var NavvoAuthError = class extends NavvoError {
  constructor(message = "Invalid or missing API key.", opts) {
    super(message, 401, "unauthorized", opts);
    this.name = "NavvoAuthError";
  }
};
var NavvoScopeError = class extends NavvoError {
  constructor(missingScope, opts) {
    super(
      opts?.message ?? (missingScope ? `API key is missing the "${missingScope}" scope.` : "API key is missing a required scope."),
      403,
      "missing_scope",
      opts
    );
    this.name = "NavvoScopeError";
    this.missingScope = missingScope;
  }
};
var NavvoForbiddenError = class extends NavvoError {
  constructor(message = "Forbidden \u2014 key revoked or access denied.", code = "forbidden", opts) {
    super(message, 403, code, opts);
    this.name = "NavvoForbiddenError";
  }
};
var NavvoNotFoundError = class extends NavvoError {
  constructor(message = "Not found.", opts) {
    super(message, 404, "not_found", opts);
    this.name = "NavvoNotFoundError";
  }
};
var NavvoRateLimitError = class extends NavvoError {
  constructor(message = "Rate limit / quota exceeded.", opts) {
    super(message, 429, opts?.code ?? "quota_exceeded", opts);
    this.name = "NavvoRateLimitError";
    this.retryAfterMs = opts?.retryAfterMs;
  }
};
var NavvoServerError = class extends NavvoError {
  constructor(message = "Navvo server error.", status = 500, opts) {
    super(message, status, "server_error", opts);
    this.name = "NavvoServerError";
  }
};
var NavvoNetworkError = class extends NavvoError {
  constructor(message = "Network error or timeout.", opts) {
    super(message, 0, "network_error", { path: opts?.path });
    this.name = "NavvoNetworkError";
    if (opts?.cause !== void 0) this.cause = opts.cause;
  }
};
var NavvoUnavailableError = class extends NavvoError {
  constructor(message = "Upstream service is temporarily unavailable.", opts) {
    super(message, 200, "unavailable", opts);
    this.name = "NavvoUnavailableError";
  }
};
function extractErrorInfo(body) {
  if (!body || typeof body !== "object") return {};
  const b = body;
  const message = typeof b.message === "string" && b.message || typeof b.error === "string" && b.error || typeof b.detail === "string" && b.detail || void 0;
  const code = typeof b.code === "string" && b.code || typeof b.error === "string" && b.error || void 0;
  let missingScope = typeof b.scope === "string" && b.scope || typeof b.requiredScope === "string" && b.requiredScope || typeof b.missingScope === "string" && b.missingScope || void 0;
  if (!missingScope && message) {
    const m = /scope[s]?[:\s"']*([a-z_]+)/i.exec(message);
    if (m) missingScope = m[1];
  }
  return { message, code, missingScope };
}

// src/core/http.ts
var DEFAULT_BASE_URL = "https://navvo.io/api/v1";
var SDK_VERSION = "0.1.0";
var DEFAULT_RETRY = {
  retries: 3,
  baseDelayMs: 500,
  factor: 2,
  jitter: true,
  maxDelayMs: 2e4
};
function sleep(ms, signal) {
  return new Promise((resolve, reject) => {
    const t = setTimeout(resolve, ms);
    if (signal) {
      const onAbort = () => {
        clearTimeout(t);
        reject(new DOMException("Aborted", "AbortError"));
      };
      if (signal.aborted) onAbort();
      else signal.addEventListener("abort", onAbort, { once: true });
    }
  });
}
function maskKey(key) {
  if (!key) return "(none)";
  return key.length > 8 ? `${key.slice(0, 5)}\u2026${key.slice(-4)}` : "nvvo_\u2026";
}
var HttpClient = class {
  constructor(config) {
    if (!config || !config.apiKey) {
      throw new NavvoError("A Navvo apiKey is required (nvvo_\u2026).", 0, "config_error");
    }
    const base = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
    if (!/^https:\/\//i.test(base)) {
      throw new NavvoError("baseUrl must be https://", 0, "config_error");
    }
    this.apiKey = config.apiKey;
    this.baseUrl = base;
    this.timeoutMs = config.timeoutMs ?? 15e3;
    this.retry = { ...DEFAULT_RETRY, ...config.retry ?? {} };
    this.userAgent = config.userAgent;
    this.language = config.language;
  }
  get maskedKey() {
    return maskKey(this.apiKey);
  }
  buildUrl(path, query) {
    const url = new URL(this.baseUrl + (path.startsWith("/") ? path : `/${path}`));
    if (query) {
      for (const [k, v] of Object.entries(query)) {
        if (v === void 0 || v === null) continue;
        url.searchParams.set(k, String(v));
      }
    }
    return url.toString();
  }
  async get(path, opts = {}) {
    return this.request("GET", path, { ...opts, idempotent: true });
  }
  async post(path, body, opts = {}) {
    return this.request("POST", path, { ...opts, body });
  }
  async request(method, path, opts = {}) {
    const url = this.buildUrl(path, opts.query);
    const okStatuses = opts.okStatuses ?? [200, 201];
    const retrySafe = method === "GET" || opts.idempotent === true;
    const maxAttempts = retrySafe ? this.retry.retries + 1 : 1;
    let lastErr;
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      try {
        const res = await this.doFetch(method, url, opts);
        const status = res.status;
        if (okStatuses.includes(status)) {
          return await this.parseBody(res);
        }
        const bodyText = await safeText(res);
        const parsed = safeJson(bodyText);
        const err = this.mapHttpError(status, parsed ?? bodyText, path, res.headers);
        if (this.isRetryable(status) && attempt < maxAttempts - 1) {
          lastErr = err;
          await sleep(this.backoff(attempt, err), opts.signal);
          continue;
        }
        throw err;
      } catch (e) {
        if (e instanceof NavvoError && !this.isRetryable(e.status)) throw e;
        lastErr = e instanceof NavvoError ? e : this.mapNetworkError(e, path);
        if (attempt < maxAttempts - 1) {
          await sleep(this.backoff(attempt), opts.signal);
          continue;
        }
        throw lastErr;
      }
    }
    throw lastErr instanceof Error ? lastErr : new NavvoError("Request failed", 0, "error", { path });
  }
  async doFetch(method, url, opts) {
    const controller = new AbortController();
    const timeout = setTimeout(() => controller.abort(), this.timeoutMs);
    if (opts.signal) {
      if (opts.signal.aborted) controller.abort();
      else opts.signal.addEventListener("abort", () => controller.abort(), { once: true });
    }
    const headers = {
      "x-api-key": this.apiKey,
      accept: "application/json"
    };
    if (this.userAgent) headers["user-agent"] = this.userAgent;
    if (this.language) headers["accept-language"] = this.language;
    let bodyInit;
    if (opts.body !== void 0) {
      headers["content-type"] = "application/json";
      bodyInit = JSON.stringify(opts.body);
    }
    try {
      return await fetch(url, { method, headers, body: bodyInit, signal: controller.signal });
    } finally {
      clearTimeout(timeout);
    }
  }
  async parseBody(res) {
    const ct = res.headers.get("content-type") || "";
    if (res.status === 204) return null;
    if (ct.includes("application/json") || ct.includes("+json")) {
      return res.json();
    }
    const text = await res.text();
    return safeJson(text) ?? text;
  }
  mapHttpError(status, body, path, headers) {
    const info = extractErrorInfo(body);
    const opts = { path, body };
    if (status === 401) return new NavvoAuthError(info.message, opts);
    if (status === 403) {
      const code = (info.code || "").toLowerCase();
      const msg = (info.message || "").toLowerCase();
      if (code.includes("quota") || msg.includes("quota") || msg.includes("rate")) {
        return new NavvoRateLimitError(info.message, { ...opts, code: "quota_exceeded" });
      }
      if (info.missingScope || code.includes("scope") || msg.includes("scope")) {
        return new NavvoScopeError(info.missingScope, { ...opts, message: info.message });
      }
      return new NavvoForbiddenError(info.message || void 0, info.code || "forbidden", opts);
    }
    if (status === 404) return new NavvoNotFoundError(info.message, opts);
    if (status === 429) {
      return new NavvoRateLimitError(info.message, {
        ...opts,
        retryAfterMs: parseRetryAfter(headers.get("retry-after"))
      });
    }
    if (status >= 500) return new NavvoServerError(info.message, status, opts);
    return new NavvoError(info.message || `HTTP ${status}`, status, info.code || "http_error", opts);
  }
  mapNetworkError(e, path) {
    const name = e?.name;
    if (name === "AbortError") {
      return new NavvoNetworkError(`Request to ${path} timed out after ${this.timeoutMs}ms.`, { path, cause: e });
    }
    return new NavvoNetworkError(`Network error calling ${path}.`, { path, cause: e });
  }
  isRetryable(status) {
    return status === 0 || status === 429 || status >= 500;
  }
  backoff(attempt, err) {
    if (err instanceof NavvoRateLimitError && err.retryAfterMs) {
      return Math.min(err.retryAfterMs, this.retry.maxDelayMs);
    }
    const raw = this.retry.baseDelayMs * Math.pow(this.retry.factor, attempt);
    const capped = Math.min(raw, this.retry.maxDelayMs);
    return this.retry.jitter ? Math.random() * capped : capped;
  }
};
function parseRetryAfter(v) {
  if (!v) return void 0;
  const secs = Number(v);
  if (Number.isFinite(secs)) return secs * 1e3;
  const date = Date.parse(v);
  if (!Number.isNaN(date)) return Math.max(0, date - Date.now());
  return void 0;
}
async function safeText(res) {
  try {
    return await res.text();
  } catch {
    return "";
  }
}
function safeJson(text) {
  if (!text) return null;
  try {
    return JSON.parse(text);
  } catch {
    return null;
  }
}

// src/core/polyline.ts
function decodePolyline(str, precision = 6) {
  if (!str) return [];
  const factor = Math.pow(10, precision);
  let index = 0;
  let lat = 0;
  let lng = 0;
  const coordinates = [];
  while (index < str.length) {
    let result = 1;
    let shift = 0;
    let b;
    do {
      b = str.charCodeAt(index++) - 63 - 1;
      result += b << shift;
      shift += 5;
    } while (b >= 31);
    lat += result & 1 ? ~(result >> 1) : result >> 1;
    result = 1;
    shift = 0;
    do {
      b = str.charCodeAt(index++) - 63 - 1;
      result += b << shift;
      shift += 5;
    } while (b >= 31);
    lng += result & 1 ? ~(result >> 1) : result >> 1;
    coordinates.push([lng / factor, lat / factor]);
  }
  return coordinates;
}
function decodePolyline6(str) {
  return decodePolyline(str, 6);
}
function encodePolyline(coords, precision = 6) {
  if (!coords.length) return "";
  const factor = Math.pow(10, precision);
  let output = "";
  let prevLat = 0;
  let prevLng = 0;
  const encodeValue = (current, previous) => {
    let coordinate = Math.round(current * factor) - Math.round(previous * factor);
    coordinate <<= 1;
    if (coordinate < 0) coordinate = ~coordinate;
    let out = "";
    while (coordinate >= 32) {
      out += String.fromCharCode((32 | coordinate & 31) + 63);
      coordinate >>= 5;
    }
    out += String.fromCharCode(coordinate + 63);
    return out;
  };
  for (const [lng, lat] of coords) {
    output += encodeValue(lat, prevLat);
    output += encodeValue(lng, prevLng);
    prevLat = lat;
    prevLng = lng;
  }
  return output;
}

// src/core/style-url.ts
function resolveStyleName(style) {
  const s = String(style).toLowerCase();
  switch (s) {
    case "road":
      return "standard";
    case "light":
      return "positron";
    default:
      return s;
  }
}
function styleUrl(style = "standard", opts) {
  const base = (opts.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
  if (/^https?:\/\//i.test(style)) {
    return injectApiKey(style, opts.apiKey);
  }
  const url = new URL(`${base}/tiles/style`);
  url.searchParams.set("style", resolveStyleName(style));
  if (opts.terrain) url.searchParams.set("terrain", "1");
  if (opts.version) url.searchParams.set("version", opts.version);
  if (opts.apiKey) url.searchParams.set("api_key", opts.apiKey);
  return url.toString();
}
function injectApiKey(rawUrl, apiKey) {
  if (!apiKey) return rawUrl;
  try {
    const u = new URL(rawUrl);
    if (!u.searchParams.has("api_key") && !u.searchParams.has("apiKey")) {
      u.searchParams.set("api_key", apiKey);
    }
    return u.toString();
  } catch {
    if (/[?&]api_?key=/i.test(rawUrl)) return rawUrl;
    return rawUrl + (rawUrl.includes("?") ? "&" : "?") + "api_key=" + encodeURIComponent(apiKey);
  }
}
var METHOD_SCOPES = {
  styles: ["tiles"],
  styleUrl: ["tiles"],
  mapConfig: ["tiles"],
  layers: ["tiles"],
  categoryStyles: ["tiles"],
  search: ["search"],
  geocode: ["geocode"],
  reverseGeocode: ["geocode"],
  geocodeBatch: ["geocode"],
  reverseGeocodeBatch: ["geocode"],
  place: ["places"],
  placeThumb: ["places"],
  placeHours: ["places"],
  placePhotos: ["places"],
  placeCategories: ["places"],
  placeFacets: ["places"],
  placesList: ["places"],
  reachable: ["places"],
  route: ["routing"],
  simpleRoute: ["routing"],
  matrix: ["routing"],
  isochrone: ["routing"],
  mapMatch: ["routing"],
  optimize: ["optimize", "routing"],
  trafficRoute: ["routing", "traffic"],
  trafficSummary: ["traffic"],
  trafficLayers: ["traffic"],
  incidents: ["traffic"],
  incidentTypes: ["traffic"],
  reportIncident: ["traffic"],
  voteIncident: ["traffic"],
  roadEdits: ["tiles"],
  routeRisk: ["routing"],
  routeEnforcement: ["routing"],
  weatherPoint: ["tiles"],
  weatherForecast: ["tiles"],
  weatherAlerts: ["tiles"],
  weatherCities: ["tiles"],
  airQualityCities: ["tiles"],
  overlay: ["tiles"],
  evStations: ["geo"],
  transitStops: ["geo"],
  gbfs: ["geo"],
  mapFeatures: ["imagery"]
};

// src/services/directions.ts
var Directions = class {
  constructor(services) {
    this.services = services;
  }
  /** POST /routing/plan — corridor-aware directions. Returns the normalised result. */
  plan(input) {
    return this.services.route(input);
  }
  /** POST /routing/route — simple/fast directions (no corridor avoidance). */
  simple(input) {
    return this.services.simpleRoute(input);
  }
  /** POST /traffic/route — traffic-aware ETA + congestion-coloured segments. */
  trafficRoute(input) {
    return this.services.trafficRoute(input);
  }
  /** Build a flat turn list from a route plan result (all legs concatenated). */
  getTurnList(route) {
    const items = [];
    for (const leg of route.legs) {
      for (const m of leg.maneuvers) {
        items.push({
          icon: maneuverIcon(m.type, m.roundaboutExitCount),
          text: m.instruction,
          distanceText: formatDistance(m.length),
          durationText: formatDuration(m.time),
          lengthKm: m.length,
          timeSec: m.time,
          type: m.type
        });
      }
    }
    return items;
  }
};
function maneuverIcon(type, roundaboutExit) {
  switch (type) {
    case 0:
      return "none";
    case 1:
    // start
    case 2:
    // start-right
    case 3:
      return "depart";
    case 4:
    // destination
    case 5:
    // destination-right
    case 6:
      return "arrive";
    case 7:
    // becomes
    case 8:
      return "straight";
    case 9:
      return "turn-slight-right";
    case 10:
      return "turn-right";
    case 11:
      return "turn-sharp-right";
    case 12:
    // uturn-right
    case 13:
      return "uturn";
    case 14:
      return "turn-sharp-left";
    case 15:
      return "turn-left";
    case 16:
      return "turn-slight-left";
    case 17:
      return "ramp-straight";
    case 18:
      return "ramp-right";
    case 19:
      return "ramp-left";
    case 20:
      return "exit-right";
    case 21:
      return "exit-left";
    case 22:
    // stay-straight
    case 23:
    // stay-right
    case 24:
      return "merge";
    case 25:
      return "merge";
    case 26:
    // roundabout-enter
    case 27:
      return roundaboutExit ? `roundabout-${roundaboutExit}` : "roundabout";
    case 37:
      return "merge-right";
    case 38:
      return "merge-left";
    default:
      return "straight";
  }
}
function formatDistance(km) {
  if (!Number.isFinite(km) || km <= 0) return "";
  if (km < 1) return `${Math.round(km * 1e3)} m`;
  return `${km.toFixed(km < 10 ? 1 : 0)} km`;
}
function formatDuration(sec) {
  if (!Number.isFinite(sec) || sec <= 0) return "";
  const mins = Math.round(sec / 60);
  if (mins < 60) return `${mins} min`;
  const h = Math.floor(mins / 60);
  const m = mins % 60;
  return m ? `${h} h ${m} min` : `${h} h`;
}

// src/services/services.ts
var NavvoServices = class {
  constructor(config) {
    this.http = new HttpClient(config);
    this.throwOnUnavailable = config.throwOnUnavailable ?? false;
    this.defaultLanguage = config.language;
    this.directions = new Directions(this);
  }
  // ---------------------------------------------------------------------------
  // Tiles / config
  // ---------------------------------------------------------------------------
  /** GET /tiles/styles — the style catalog (for a style switcher). */
  styles() {
    return this.http.get("/tiles/styles");
  }
  /** Build a tiles/style URL with the api_key query param. */
  styleUrl(style = "standard", opts) {
    return styleUrl(style, { apiKey: this.http.apiKey, baseUrl: this.http.baseUrl, ...opts });
  }
  /** GET /tiles/layers — logical layer catalog. */
  layers() {
    return this.http.get("/tiles/layers");
  }
  /** GET /map/config — the bootstrap manifest. Cached after the first call. */
  mapConfig() {
    if (!this.configCache) this.configCache = this.http.get("/map/config");
    return this.configCache;
  }
  /** GET /map/category-styles — renderer-ready category marker styling. */
  categoryStyles() {
    return this.http.get("/map/category-styles");
  }
  // ---------------------------------------------------------------------------
  // Search
  // ---------------------------------------------------------------------------
  /** GET /search — full-text place search. */
  async search(q, opts = {}) {
    const bbox = opts.bbox ? bboxParam(opts.bbox) : void 0;
    const raw = await this.http.get("/search", {
      query: {
        q,
        limit: opts.limit,
        page: opts.page,
        locale: opts.locale ?? opts.lang ?? this.defaultLanguage,
        bbox
      }
    });
    const places = raw.places ?? {};
    const items = (places.items ?? []).map((it) => this.normalizePlaceSummary(it));
    return {
      query: raw.query ?? q,
      locale: raw.locale ?? opts.locale ?? "en",
      items,
      total: places.total ?? items.length,
      page: places.page ?? 1,
      limit: places.limit ?? opts.limit ?? 25,
      understanding: raw.understanding,
      raw
    };
  }
  normalizePlaceSummary(it) {
    const center = parseWkt(it.center) ?? [0, 0];
    return {
      id: String(it.id ?? ""),
      slug: String(it.slug ?? ""),
      nameEn: String(it.nameEn ?? ""),
      nameAr: String(it.nameAr ?? ""),
      categories: it.categories ?? [],
      canonicalCategorySlug: it.canonicalCategorySlug,
      rawCategories: it.rawCategories,
      rating: Number(it.rating ?? 0),
      sponsored: Boolean(it.sponsored),
      lngLat: center
    };
  }
  // ---------------------------------------------------------------------------
  // Geocoding
  // ---------------------------------------------------------------------------
  /** GET /geocode/search — forward geocoding. */
  async geocode(q, opts = {}) {
    let lat;
    let lon;
    if (opts.near) {
      const [lng, la] = toLngLat(opts.near);
      lat = String(la);
      lon = String(lng);
    }
    const env = await this.http.get(
      "/geocode/search",
      {
        query: {
          q,
          limit: opts.limit,
          locale: opts.locale ?? this.defaultLanguage,
          bbox: opts.bbox ? bboxParam(opts.bbox) : void 0,
          lat,
          lon
        }
      }
    );
    return this.wrapGeocodeEnvelope(env, env.data ?? [], "/geocode/search");
  }
  /** GET /geocode/reverse — reverse geocoding (separate lat & lon params). */
  async reverseGeocode(coord) {
    const [lng, lat] = toLngLat(coord);
    const env = await this.http.get(
      "/geocode/reverse",
      { query: { lat: String(lat), lon: String(lng) } }
    );
    const list = env.data ? [env.data] : [];
    return this.wrapGeocodeEnvelope(env, list, "/geocode/reverse");
  }
  /** POST /geocode/batch — bulk forward geocode (max 50). */
  async geocodeBatch(queries, opts = {}) {
    const raw = await this.http.post(
      "/geocode/batch",
      { queries, locale: opts.locale ?? this.defaultLanguage, bbox: opts.bbox ? bboxParam(opts.bbox) : void 0 },
      { idempotent: true }
    );
    return {
      count: raw.count,
      found: raw.found,
      results: (raw.results ?? []).map((r) => ({
        query: String(r.query ?? ""),
        found: Boolean(r.found),
        lngLat: r.found ? [num(r.lng) ?? 0, num(r.lat) ?? 0] : null,
        displayName: r.displayName,
        type: r.type,
        category: r.category
      }))
    };
  }
  /** POST /geocode/reverse-batch — bulk reverse geocode (max 50). */
  async reverseGeocodeBatch(points) {
    const raw = await this.http.post(
      "/geocode/reverse-batch",
      { points: points.map((p) => toLatLngObj(p)) },
      { idempotent: true }
    );
    return {
      count: raw.count,
      found: raw.found,
      results: (raw.results ?? []).map((r) => ({
        lngLat: [num(r.lng) ?? 0, num(r.lat) ?? 0],
        found: Boolean(r.found),
        displayName: r.displayName,
        type: r.type,
        address: r.address
      }))
    };
  }
  wrapGeocodeEnvelope(env, rows, path) {
    if (!env.available) {
      this.maybeThrowUnavailable(path, env);
      return { provider: env.provider, available: false, results: [], status: env.status };
    }
    const results = rows.map((d) => {
      const lat = num(d.lat) ?? 0;
      const lon = num(d.lon) ?? 0;
      const bb = d.boundingbox;
      let bounds;
      if (bb && bb.length === 4) {
        const minLat = num(bb[0]) ?? 0;
        const maxLat = num(bb[1]) ?? 0;
        const minLon = num(bb[2]) ?? 0;
        const maxLon = num(bb[3]) ?? 0;
        bounds = [[minLon, minLat], [maxLon, maxLat]];
      }
      return {
        displayName: String(d.display_name ?? d.name ?? ""),
        lngLat: [lon, lat],
        bounds,
        type: d.type,
        category: d.category,
        name: d.name,
        address: d.address,
        raw: d
      };
    });
    return { provider: env.provider, available: true, results, status: env.status };
  }
  // ---------------------------------------------------------------------------
  // Places
  // ---------------------------------------------------------------------------
  /**
   * GET /places/{slug} — full place detail.
   *
   * Soft-null getter: returns `null` when the place is unknown (the API answers
   * HTTP 404, which the HTTP layer surfaces as a typed NavvoNotFoundError that we
   * catch here). Some deployments answer 200 with an empty/`null` body for an
   * unknown slug; that is normalised to `null` too. All other typed errors
   * (auth, scope, rate-limit, server, network) still propagate.
   */
  async place(slug) {
    let res;
    try {
      res = await this.http.get(`/places/${encodeURIComponent(slug)}`);
    } catch (err) {
      if (err instanceof NavvoNotFoundError) return null;
      throw err;
    }
    if (res == null || typeof res === "object" && Object.keys(res).length === 0) {
      return null;
    }
    return res;
  }
  /** GET /places/thumb?slug= — lightweight card thumbnail + open state. */
  placeThumb(slug) {
    return this.http.get("/places/thumb", { query: { slug } });
  }
  /** GET /places/{slug}/hours — opening-hours engine. */
  placeHours(slug, at) {
    return this.http.get(`/places/${encodeURIComponent(slug)}/hours`, { query: { at } });
  }
  /** GET /places/{slug}/popular-times — busyness. */
  placePopularTimes(slug) {
    return this.http.get(`/places/${encodeURIComponent(slug)}/popular-times`);
  }
  /** GET /places/{slug}/photos — gallery. */
  placePhotos(slug, opts = {}) {
    return this.http.get(`/places/${encodeURIComponent(slug)}/photos`, { query: { page: opts.page, limit: opts.limit } });
  }
  /** GET /places/categories — the category taxonomy. */
  placeCategories() {
    return this.http.get("/places/categories");
  }
  /** GET /places/facets — amenity facets. */
  placeFacets(q) {
    return this.http.get("/places/facets", { query: { q } });
  }
  /** GET /places — list places with filters. */
  placesList(opts = {}) {
    let near;
    if (opts.near) {
      const [lng, lat] = toLngLat(opts.near);
      near = `${lat},${lng}`;
    }
    return this.http.get("/places", {
      query: {
        q: opts.q,
        page: opts.page,
        limit: opts.limit,
        category: opts.category,
        amenities: opts.amenities,
        openNow: opts.openNow,
        sort: opts.sort,
        minRating: opts.minRating,
        near,
        radiusKm: opts.radiusKm
      }
    });
  }
  /** POST /places/reachable — places inside an isochrone. */
  reachable(input) {
    return this.http.post(
      "/places/reachable",
      {
        origin: toLatLngObj(input.origin),
        minutes: input.minutes,
        costing: input.costing,
        q: input.q,
        categories: Array.isArray(input.categories) ? input.categories.join(",") : input.categories,
        amenities: input.amenities,
        limit: input.limit
      },
      { idempotent: true }
    );
  }
  // ---------------------------------------------------------------------------
  // Routing
  // ---------------------------------------------------------------------------
  /** POST /routing/plan — corridor-aware high-level directions. */
  async route(input) {
    const body = this.buildPlanBody(input);
    const raw = await this.http.post("/routing/plan", body, {
      okStatuses: [200, 201],
      idempotent: true
    });
    return this.normalizePlan(raw);
  }
  /** POST /routing/route — simple/fast directions (no corridor avoidance). */
  async simpleRoute(input) {
    const payload = {
      locations: input.waypoints.map((w) => toLatLonObj(w.lngLat)),
      costing: input.costing ?? "auto",
      directions_options: { units: "kilometers", language: input.language ?? this.defaultLanguage ?? "en-US" }
    };
    const raw = await this.http.post("/routing/route", { payload }, {
      okStatuses: [200, 201],
      idempotent: true
    });
    return this.normalizePassthroughTrip(raw, raw);
  }
  /** POST /routing/matrix — travel-time/distance matrix. */
  async matrix(input) {
    const payload = {
      sources: input.origins.map((o) => toLatLonObj(o)),
      targets: input.destinations.map((d) => toLatLonObj(d)),
      costing: input.costing ?? "auto",
      units: input.units ?? "kilometers"
    };
    const env = await this.http.post(
      "/routing/matrix",
      { payload },
      { okStatuses: [200, 201], idempotent: true }
    );
    if (!env.available) {
      this.maybeThrowUnavailable("/routing/matrix", env);
      return { available: false, cells: [], raw: env };
    }
    const rows = (env.data?.sources_to_targets ?? []).map(
      (row) => row.map((cell) => ({
        distance: cell?.distance == null ? null : Number(cell.distance),
        time: cell?.time == null ? null : Number(cell.time)
      }))
    );
    return { available: true, cells: rows, raw: env };
  }
  /** POST /routing/isochrone — reachability polygons. */
  async isochrone(input) {
    const contours = input.contours.map((c) => {
      const o = {};
      if (c.minutes != null) o.time = c.minutes;
      if (c.km != null) o.distance = c.km;
      if (c.color) o.color = c.color;
      return o;
    });
    const payload = {
      locations: [toLatLonObj(input.origin)],
      costing: input.costing ?? "pedestrian",
      contours,
      polygons: input.polygons ?? true
    };
    const env = await this.http.post(
      "/routing/isochrone",
      { payload },
      { okStatuses: [200, 201], idempotent: true }
    );
    if (!env.available) {
      this.maybeThrowUnavailable("/routing/isochrone", env);
      return { available: false, geojson: null, raw: env };
    }
    return { available: true, geojson: env.data ?? null, raw: env };
  }
  /** POST /routing/map-match — snap a GPS trace to roads. */
  mapMatch(payload) {
    return this.http.post("/routing/map-match", { payload }, { okStatuses: [200, 201], idempotent: true });
  }
  /** POST /routing/optimize — optimal stop ordering (TSP). */
  async optimize(input) {
    const payload = {
      locations: input.stops.map((s) => toLatLonObj(s)),
      costing: input.costing ?? "auto"
    };
    const env = await this.http.post(
      "/routing/optimize",
      { payload },
      { okStatuses: [200, 201], idempotent: true }
    );
    if (!env.available) {
      this.maybeThrowUnavailable("/routing/optimize", env);
      return { available: false, order: [], raw: env };
    }
    const locs = env.data?.trip?.locations ?? [];
    const order = locs.map((l) => Number(l.original_index ?? 0));
    return { available: true, order, raw: env };
  }
  // ---------------------------------------------------------------------------
  // Traffic & incidents
  // ---------------------------------------------------------------------------
  /** POST /traffic/route — traffic-aware ETA + congestion-coloured segments. */
  async trafficRoute(input) {
    const raw = await this.http.post(
      "/traffic/route",
      {
        locations: input.waypoints.map((w) => toLonLatObj(w.lngLat, w.name)),
        costing: input.costing ?? "auto",
        departureTime: input.departureTime,
        language: input.language ?? this.defaultLanguage
      },
      { okStatuses: [200, 201], idempotent: true }
    );
    if (!raw.available) this.maybeThrowUnavailable("/traffic/route", raw);
    return {
      available: Boolean(raw.available),
      distanceKm: Number(raw.distanceKm ?? 0),
      baseDurationSeconds: Number(raw.baseDurationSeconds ?? 0),
      durationInTrafficSeconds: Number(raw.durationInTrafficSeconds ?? 0),
      delaySeconds: Number(raw.delaySeconds ?? 0),
      departureTime: raw.departureTime,
      arrivalTime: raw.arrivalTime,
      traffic: raw.traffic,
      segments: raw.segments ?? { type: "FeatureCollection", features: [] },
      legend: raw.legend,
      raw
    };
  }
  /** GET /traffic/summary — aggregate live conditions over a bbox. */
  trafficSummary(bbox) {
    return this.http.get("/traffic/summary", { query: { bbox: bbox ? bboxParam(bbox) : void 0 } });
  }
  /** GET /traffic/layers — live traffic layer descriptor + legend. */
  trafficLayers() {
    return this.http.get("/traffic/layers");
  }
  /** GET /incidents/geojson — active incidents as GeoJSON. */
  incidents(opts = {}) {
    return this.http.get("/incidents/geojson", {
      query: { bbox: opts.bbox ? bboxParam(opts.bbox) : void 0, type: opts.type, limit: opts.limit, zoom: opts.zoom }
    });
  }
  /** GET /incidents/types — the incident type catalog. */
  incidentTypes() {
    return this.http.get("/incidents/types");
  }
  /** POST /incidents/report — community report (auto-dedup). */
  reportIncident(input) {
    const [lng, lat] = toLngLat(input.lngLat);
    return this.http.post("/incidents/report", {
      type: input.type,
      lat,
      lng,
      title: input.title,
      description: input.description,
      severity: input.severity,
      ttlMinutes: input.ttlMinutes,
      dedupMeters: input.dedupMeters
    });
  }
  /** POST /incidents/{id}/votes — confirm/reject an incident. */
  voteIncident(id, vote, userId) {
    return this.http.post(`/incidents/${encodeURIComponent(id)}/votes`, { vote, userId });
  }
  // ---------------------------------------------------------------------------
  // Road edits & route risk
  // ---------------------------------------------------------------------------
  /** GET /road-edits/geojson — active closures/restrictions/custom roads. */
  roadEdits(bbox) {
    return this.http.get("/road-edits/geojson", { query: { bbox: bbox ? bboxParam(bbox) : void 0 } });
  }
  /** POST /map/route-risk — score a route geometry against live conditions. */
  routeRisk(geometry, opts = {}) {
    return this.http.post("/map/route-risk", { geometry, routeId: opts.routeId, timeOffset: opts.timeOffset });
  }
  /** POST /map/route-enforcement — cameras/lights along a route. */
  routeEnforcement(geometry, opts = {}) {
    return this.http.post("/map/route-enforcement", { geometry, routeId: opts.routeId });
  }
  // ---------------------------------------------------------------------------
  // Weather & air quality
  // ---------------------------------------------------------------------------
  /** GET /map/weather/point — current point weather + live air quality. */
  weatherPoint(coord, opts = {}) {
    const [lng, lat] = toLngLat(coord);
    return this.http.get("/map/weather/point", { query: { lat, lng, units: opts.units } });
  }
  /** GET /map/weather/forecast — hourly forecast. */
  weatherForecast(coord, opts = {}) {
    const [lng, lat] = toLngLat(coord);
    return this.http.get("/map/weather/forecast", { query: { lat, lng, hours: opts.hours, units: opts.units } });
  }
  /** GET /map/weather/alerts — derived warnings for a point. */
  weatherAlerts(coord, opts = {}) {
    const [lng, lat] = toLngLat(coord);
    return this.http.get("/map/weather/alerts", { query: { lat, lng, hours: opts.hours } });
  }
  /** GET /map/weather/cities — weather pins in a viewport (zoom >= 7). */
  weatherCities(bbox, zoom, opts = {}) {
    return this.http.get("/map/weather/cities", { query: { bbox: bboxParam(bbox), zoom, units: opts.units, limit: opts.limit } });
  }
  /** GET /map/airquality/cities — air-quality pins in a viewport (zoom >= 7). */
  airQualityCities(bbox, zoom, opts = {}) {
    return this.http.get("/map/airquality/cities", { query: { bbox: bboxParam(bbox), zoom, limit: opts.limit } });
  }
  // ---------------------------------------------------------------------------
  // Overlays (generic)
  // ---------------------------------------------------------------------------
  /** GET /map/overlays/{layerType} — one overlay as GeoJSON. */
  overlay(layerType, opts = {}) {
    return this.http.get(`/map/overlays/${encodeURIComponent(layerType)}`, {
      query: { bbox: opts.bbox ? bboxParam(opts.bbox) : void 0, limit: opts.limit, zoom: opts.zoom }
    });
  }
  // ---------------------------------------------------------------------------
  // Mobility (EV / transit / GBFS)
  // ---------------------------------------------------------------------------
  /** GET /ev/stations — charging stations in a viewport. */
  evStations(opts) {
    return this.http.get("/ev/stations", {
      query: {
        bbox: bboxParam(opts.bbox),
        minKw: opts.minKw,
        connector: opts.connector,
        limit: opts.limit,
        available: opts.availableOnly ? 1 : void 0
      }
    });
  }
  /** GET /transit/stops — transit stops near a point (radius in metres). */
  transitStops(coord, radius = 500) {
    const [lng, lat] = toLngLat(coord);
    return this.http.get("/transit/stops", { query: { lat, lng, radius } });
  }
  /** GET /transit/gbfs — shared docks/bikes/scooters near a point. */
  gbfs(coord, opts = {}) {
    const [lng, lat] = toLngLat(coord);
    return this.http.get("/transit/gbfs", { query: { lat, lng, radius: opts.radius, type: opts.type } });
  }
  // ---------------------------------------------------------------------------
  // Imagery
  // ---------------------------------------------------------------------------
  /** GET /imagery/map-features — signs/points from street-level imagery. */
  mapFeatures(opts = {}) {
    return this.http.get("/imagery/map-features", { query: { bbox: opts.bbox ? bboxParam(opts.bbox) : void 0 } });
  }
  // ---------------------------------------------------------------------------
  // Internal normalisers (shared with Directions)
  // ---------------------------------------------------------------------------
  buildPlanBody(input) {
    const body = {
      locations: input.waypoints.map((w) => toLonLatObj(w.lngLat, w.name)),
      costing: input.costing ?? "auto",
      preference: input.preference,
      avoid: input.avoid,
      optimize: input.optimize,
      avoidIncidents: input.avoidIncidents,
      avoidReported: input.avoidReported,
      disregard: input.disregard,
      constraintsText: input.constraintsText,
      includeLabels: input.includeLabels,
      language: input.language ?? this.defaultLanguage,
      departureTime: input.departureTime
    };
    if (input.truck) {
      body.truck = input.truck;
    }
    for (const k of Object.keys(body)) if (body[k] === void 0) delete body[k];
    return body;
  }
  /** Normalise a /routing/plan response (engine envelope), tolerant of a flat shape. */
  normalizePlan(raw) {
    const engine = raw.engine ?? raw;
    if (engine.available === false) this.maybeThrowUnavailable("/routing/plan", engine);
    const data = engine.data ?? {};
    const trip = data.trip ?? {};
    const base = this.tripToResult(trip);
    const alternatesRaw = data.alternates ?? [];
    const alternates = alternatesRaw.map((a) => this.tripToResult(a.trip ?? {}, raw));
    return {
      ...base,
      available: engine.available !== false,
      alternates,
      labels: raw.labels?.parsed ?? null,
      logistics: raw.logistics,
      signConstraints: raw.signConstraints,
      incidentConstraints: raw.incidentConstraints,
      reportedConstraints: raw.reportedConstraints,
      roadEditConstraints: raw.roadEditConstraints,
      routeProfile: raw.routeProfile,
      raw
    };
  }
  /** Normalise a passthrough /routing/route response (has `data.trip`). */
  normalizePassthroughTrip(env, raw) {
    if (env.available === false) this.maybeThrowUnavailable("/routing/route", env);
    const data = env.data ?? {};
    const trip = data.trip ?? {};
    const base = this.tripToResult(trip);
    return {
      ...base,
      available: env.available !== false,
      labels: env.labels?.parsed ?? null,
      raw
    };
  }
  /** Convert a raw engine `trip` object to a normalised RoutePlanResult (no envelope fields). */
  tripToResult(trip, raw = trip) {
    const legsRaw = trip.legs ?? [];
    const legs = legsRaw.map((leg) => {
      const shape = String(leg.shape ?? "");
      const geometry2 = shape ? decodePolyline6(shape) : [];
      const maneuvers = (leg.maneuvers ?? []).map((m) => ({
        type: Number(m.type ?? 0),
        instruction: String(m.instruction ?? ""),
        verbalInstruction: m.verbal_pre_transition_instruction ?? m.verbal_transition_alert_instruction ?? void 0,
        streetNames: m.street_names,
        length: Number(m.length ?? 0),
        time: Number(m.time ?? 0),
        roundaboutExitCount: m.roundabout_exit_count,
        beginShapeIndex: m.begin_shape_index,
        raw: m
      }));
      const summary = leg.summary ?? {};
      return {
        maneuvers,
        geometry: geometry2,
        shape,
        summary: { length: Number(summary.length ?? 0), time: Number(summary.time ?? 0) }
      };
    });
    const tripSummary = trip.summary ?? {};
    const geometry = legs.flatMap((l) => l.geometry);
    return {
      available: true,
      geometry,
      shape: legs[0]?.shape ?? "",
      summary: { length: Number(tripSummary.length ?? 0), time: Number(tripSummary.time ?? 0) },
      legs,
      alternates: [],
      raw
    };
  }
  maybeThrowUnavailable(path, body) {
    if (this.throwOnUnavailable) {
      throw new NavvoUnavailableError(`Service for ${path} is temporarily unavailable (available:false).`, { path, body });
    }
  }
};

export { DEFAULT_BASE_URL, Directions, METHOD_SCOPES, NavvoAuthError, NavvoError, NavvoForbiddenError, NavvoNetworkError, NavvoNotFoundError, NavvoRateLimitError, NavvoScopeError, NavvoServerError, NavvoServices, NavvoUnavailableError, SDK_VERSION, bboxParam, decodePolyline, decodePolyline6, encodePolyline, formatDistance, formatDuration, injectApiKey, latLonParams, maneuverIcon, parseWkt, resolveStyleName, styleUrl, toLatLngObj, toLatLonObj, toLngLat, toLonLatObj };
//# sourceMappingURL=services.mjs.map
//# sourceMappingURL=services.mjs.map