'use strict';

// src/errors.ts
var NavvoError = class extends Error {
  /** HTTP status code, when the error originated from a response. */
  status;
  /** Request path (relative to the base URL), e.g. `/geocode/forward`. */
  path;
  /** HTTP method of the failing request. */
  method;
  /** Server-provided request id, when present, for support correlation. */
  requestId;
  /** The API key, redacted to `nvvo_****<last4>` — never the full key. */
  redactedKey;
  /** The parsed response body (untyped) for forward-compat / debugging. */
  body;
  constructor(message, init = {}) {
    super(message, init.cause !== void 0 ? { cause: init.cause } : void 0);
    this.name = new.target.name;
    this.status = init.status;
    this.path = init.path;
    this.method = init.method;
    this.requestId = init.requestId;
    this.redactedKey = init.redactedKey;
    this.body = init.body;
    Object.setPrototypeOf(this, new.target.prototype);
  }
};
var NavvoConfigError = class extends NavvoError {
};
var NavvoValidationError = class extends NavvoError {
  /** Offending fields, when the server enumerates them. */
  fields;
  constructor(message, init = {}) {
    super(message, init);
    this.fields = init.fields;
  }
};
var NavvoAuthError = class extends NavvoError {
};
var NavvoForbiddenError = class extends NavvoError {
  reason;
  scope;
  constructor(message, init = {}) {
    super(message, init);
    this.reason = init.reason;
    this.scope = init.scope;
  }
};
var NavvoNotFoundError = class extends NavvoError {
};
var NavvoRateLimitError = class extends NavvoError {
  /** Seconds to wait before retrying, from `Retry-After` when present. */
  retryAfter;
  limit;
  remaining;
  resetAt;
  constructor(message, init = {}) {
    super(message, init);
    this.retryAfter = init.retryAfter;
    this.limit = init.limit;
    this.remaining = init.remaining;
    this.resetAt = init.resetAt;
  }
};
var NavvoServiceUnavailableError = class extends NavvoError {
};
var NavvoServerError = class extends NavvoError {
};
var NavvoTimeoutError = class extends NavvoError {
  /** Number of attempts made before giving up. */
  attempts;
  constructor(message, init = {}) {
    super(message, init);
    this.attempts = init.attempts;
  }
};
var NavvoNetworkError = class extends NavvoError {
};
var NavvoAbortError = class extends NavvoError {
};

// src/config.ts
var API_KEY_PATTERN = /^nvvo_[0-9a-f]{48}$/;
var SDK_VERSION = "0.1.0";
var DEFAULT_BASE_URL = "https://navvo.io/api/v1";
var DEFAULT_RETRY = {
  retries: 3,
  baseDelayMs: 500,
  factor: 2,
  jitter: true,
  maxDelayMs: 2e4,
  respectRetryAfter: true,
  retryUnavailable: false
};
function redactKey(key) {
  if (!key) return "nvvo_****";
  const last4 = key.slice(-4);
  return `nvvo_****${last4}`;
}
function buildUserAgent(suffix) {
  const runtime = typeof process !== "undefined" && process.versions?.node ? ` (node/${process.versions.node})` : "";
  const base = `navvo-sdk-node/${SDK_VERSION}${runtime}`;
  return suffix ? `${base} ${suffix}` : base;
}
function resolveConfig(options = {}) {
  const apiKey = options.apiKey ?? (typeof process !== "undefined" ? process.env?.NAVVO_API_KEY : void 0);
  if (!apiKey) {
    throw new NavvoConfigError(
      "No API key provided. Pass `apiKey` or set the NAVVO_API_KEY environment variable."
    );
  }
  if (!API_KEY_PATTERN.test(apiKey)) {
    throw new NavvoConfigError(
      `Malformed API key (${redactKey(apiKey)}). Expected the form nvvo_<48 hex chars>.`
    );
  }
  const baseUrlRaw = options.baseUrl ?? DEFAULT_BASE_URL;
  let baseUrl;
  try {
    const u = new URL(baseUrlRaw);
    if (u.protocol !== "https:" && !options.allowInsecure) {
      throw new NavvoConfigError(
        `Refusing a non-HTTPS baseUrl (${baseUrlRaw}). Set allowInsecure:true only for local testing.`
      );
    }
    baseUrl = baseUrlRaw.replace(/\/+$/, "");
  } catch (e) {
    if (e instanceof NavvoConfigError) throw e;
    throw new NavvoConfigError(`Invalid baseUrl: ${baseUrlRaw}`);
  }
  const fetchImpl = options.fetch ?? (typeof fetch !== "undefined" ? fetch.bind(globalThis) : void 0);
  if (!fetchImpl) {
    throw new NavvoConfigError(
      "No global fetch found. Use Node 18+ or pass a `fetch` implementation in the client options."
    );
  }
  const timeoutMs = options.timeoutMs ?? 15e3;
  if (timeoutMs <= 0) throw new NavvoConfigError("timeoutMs must be > 0.");
  const retry = { ...DEFAULT_RETRY, ...options.retry ?? {} };
  if (retry.retries < 0) throw new NavvoConfigError("retry.retries must be >= 0.");
  return Object.freeze({
    apiKey,
    baseUrl,
    timeoutMs,
    retry: Object.freeze(retry),
    userAgent: buildUserAgent(options.userAgent),
    defaultHeaders: { ...options.defaultHeaders ?? {} },
    telemetry: options.telemetry ?? { enabled: true },
    fetch: fetchImpl,
    redactedKey: redactKey(apiKey)
  });
}

// src/geo.ts
function assertFinite(n, label) {
  if (typeof n !== "number" || !Number.isFinite(n)) {
    throw new TypeError(`Invalid ${label}: expected a finite number, got ${String(n)}`);
  }
  return n;
}
function assertLatLng(p) {
  const lat = assertFinite(p?.lat, "lat");
  const lng = assertFinite(p?.lng, "lng");
  if (lat < -90 || lat > 90) throw new RangeError(`lat out of range [-90,90]: ${lat}`);
  if (lng < -180 || lng > 180) throw new RangeError(`lng out of range [-180,180]: ${lng}`);
  return p;
}
function toLatLon(p) {
  assertLatLng(p);
  return { lat: p.lat, lon: p.lng };
}
function toLonLat(p) {
  assertLatLng(p);
  return p.name !== void 0 ? { lon: p.lng, lat: p.lat, name: p.name } : { lon: p.lng, lat: p.lat };
}
function toLatLngObj(p) {
  assertLatLng(p);
  return { lat: p.lat, lng: p.lng };
}
function bboxToString(b) {
  return [
    assertFinite(b.west, "bbox.west"),
    assertFinite(b.south, "bbox.south"),
    assertFinite(b.east, "bbox.east"),
    assertFinite(b.north, "bbox.north")
  ].join(",");
}
function fromGeoJSON(coord) {
  return { lng: coord[0], lat: coord[1] };
}
function toGeoJSON(p) {
  assertLatLng(p);
  return [p.lng, p.lat];
}
function parseWktPoint(wkt) {
  if (!wkt) return null;
  const m = /POINT\s*\(\s*(-?\d+(?:\.\d+)?)\s+(-?\d+(?:\.\d+)?)\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 { lat, lng };
}
function decodePolyline(encoded, precision = 6) {
  if (!encoded) return [];
  const factor = Math.pow(10, precision);
  const points = [];
  let index = 0;
  let lat = 0;
  let lng = 0;
  const len = encoded.length;
  while (index < len) {
    let result = 1;
    let shift = 0;
    let b;
    do {
      b = encoded.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 = encoded.charCodeAt(index++) - 63 - 1;
      result += b << shift;
      shift += 5;
    } while (b >= 31);
    lng += result & 1 ? ~(result >> 1) : result >> 1;
    points.push({ lat: lat / factor, lng: lng / factor });
  }
  return points;
}

// src/http.ts
var sleep = (ms, signal) => new Promise((resolve, reject) => {
  if (signal?.aborted) {
    reject(new NavvoAbortError("Request aborted by caller."));
    return;
  }
  const t = setTimeout(() => {
    signal?.removeEventListener("abort", onAbort);
    resolve();
  }, ms);
  const onAbort = () => {
    clearTimeout(t);
    reject(new NavvoAbortError("Request aborted by caller."));
  };
  signal?.addEventListener("abort", onAbort, { once: true });
});
function buildUrl(baseUrl, path, query) {
  const p = path.startsWith("/") ? path : `/${path}`;
  const url = new URL(baseUrl + p);
  if (query) {
    for (const [key, value] of Object.entries(query)) {
      if (value === void 0 || value === null) continue;
      if (Array.isArray(value)) {
        url.searchParams.set(key, value.map(String).join(","));
      } else {
        url.searchParams.set(key, String(value));
      }
    }
  }
  return url.toString();
}
function parseRetryAfter(header) {
  if (!header) return void 0;
  const secs = Number(header);
  if (Number.isFinite(secs)) return Math.max(0, secs);
  const date = Date.parse(header);
  if (Number.isFinite(date)) return Math.max(0, (date - Date.now()) / 1e3);
  return void 0;
}
function readRateLimitHeaders(headers) {
  const num = (v) => {
    if (v == null) return void 0;
    const n = Number(v);
    return Number.isFinite(n) ? n : void 0;
  };
  const limit = num(headers.get("ratelimit-limit") ?? headers.get("x-ratelimit-limit"));
  const remaining = num(
    headers.get("ratelimit-remaining") ?? headers.get("x-ratelimit-remaining")
  );
  const resetRaw = headers.get("ratelimit-reset") ?? headers.get("x-ratelimit-reset");
  let resetAt;
  if (resetRaw != null) {
    const n = Number(resetRaw);
    if (Number.isFinite(n)) {
      resetAt = n < 1e6 ? new Date(Date.now() + n * 1e3) : new Date(n * 1e3);
    } else {
      const d = Date.parse(resetRaw);
      if (Number.isFinite(d)) resetAt = new Date(d);
    }
  }
  return { limit, remaining, resetAt };
}
function isRetryableStatus(status) {
  return status === 408 || status === 429 || status === 500 || status === 502 || status === 503 || status === 504;
}
var HttpTransport = class {
  constructor(config) {
    this.config = config;
  }
  config;
  emit(kind, event) {
    const tel = this.config.telemetry;
    if (tel?.enabled === false) return;
    try {
      tel?.[kind]?.(event);
    } catch {
    }
  }
  backoffDelay(attempt, retryAfter) {
    const { baseDelayMs, factor, maxDelayMs, jitter, respectRetryAfter } = this.config.retry;
    const exp = Math.min(maxDelayMs, baseDelayMs * Math.pow(factor, attempt));
    const jittered = jitter ? Math.random() * exp : exp;
    if (respectRetryAfter && retryAfter !== void 0) {
      return Math.max(retryAfter * 1e3, jittered);
    }
    return jittered;
  }
  /**
   * Execute a request descriptor, returning the parsed (envelope-unwrapped
   * where applicable) JSON body. Throws a typed {@link NavvoError} on failure.
   */
  async request(desc, opts = {}) {
    const url = buildUrl(this.config.baseUrl, desc.path, desc.query);
    const maxAttempts = this.config.retry.retries + 1;
    let lastError;
    for (let attempt = 0; attempt < maxAttempts; attempt++) {
      const started = Date.now();
      const evBase = {
        method: desc.method,
        path: desc.path,
        attempt,
        redactedKey: this.config.redactedKey
      };
      this.emit("onRequest", evBase);
      const controller = new AbortController();
      const timeoutMs = opts.timeoutMs ?? this.config.timeoutMs;
      let timedOut = false;
      const timer = setTimeout(() => {
        timedOut = true;
        controller.abort(new Error("timeout"));
      }, timeoutMs);
      const onCallerAbort = () => controller.abort(new Error("caller-abort"));
      if (opts.signal) {
        if (opts.signal.aborted) {
          clearTimeout(timer);
          throw new NavvoAbortError("Request aborted by caller.", {
            path: desc.path,
            method: desc.method,
            redactedKey: this.config.redactedKey
          });
        }
        opts.signal.addEventListener("abort", onCallerAbort, { once: true });
      }
      try {
        const headers = {
          "x-api-key": this.config.apiKey,
          "user-agent": this.config.userAgent,
          accept: "application/json",
          ...this.config.defaultHeaders,
          ...opts.headers ?? {}
        };
        let bodyStr;
        if (desc.body !== void 0 && desc.method !== "GET") {
          bodyStr = JSON.stringify(desc.body);
          headers["content-type"] = "application/json";
        }
        const res = await this.config.fetch(url, {
          method: desc.method,
          headers,
          body: bodyStr,
          signal: controller.signal
        });
        clearTimeout(timer);
        opts.signal?.removeEventListener("abort", onCallerAbort);
        const requestId = res.headers.get("x-request-id") ?? res.headers.get("x-navvo-request-id") ?? void 0;
        const durationMs = Date.now() - started;
        const rl = readRateLimitHeaders(res.headers);
        const okStatuses = desc.okStatuses;
        const isOk = okStatuses ? okStatuses.includes(res.status) : res.ok;
        const text = await res.text();
        let parsed = void 0;
        if (text) {
          try {
            parsed = JSON.parse(text);
          } catch {
            parsed = text;
          }
        }
        if (isOk) {
          if (parsed && typeof parsed === "object" && "available" in parsed && parsed.available === false) {
            if (this.config.retry.retryUnavailable && attempt < maxAttempts - 1) {
              const delay = this.backoffDelay(attempt);
              this.emit("onRetry", { ...evBase, statusCode: res.status, durationMs });
              await sleep(delay, opts.signal);
              continue;
            }
            const envStatus = parsed.status;
            this.emit("onResponse", { ...evBase, statusCode: res.status, durationMs, requestId });
            throw new NavvoServiceUnavailableError(
              `Navvo upstream engine unavailable (path=${desc.path}${envStatus ? `, upstream status=${envStatus}` : ""}).`,
              {
                status: res.status,
                path: desc.path,
                method: desc.method,
                requestId,
                redactedKey: this.config.redactedKey,
                body: parsed
              }
            );
          }
          this.emit("onResponse", { ...evBase, statusCode: res.status, durationMs, requestId });
          return parsed;
        }
        const errBody = parsed && typeof parsed === "object" ? parsed : {};
        const serverMsg = errBody.message || errBody.error || (typeof parsed === "string" ? parsed : void 0);
        const retryAfter = parseRetryAfter(res.headers.get("retry-after"));
        const shouldRetry = isRetryableStatus(res.status) && attempt < maxAttempts - 1;
        if (shouldRetry) {
          const delay = this.backoffDelay(attempt, retryAfter);
          this.emit("onRetry", {
            ...evBase,
            statusCode: res.status,
            durationMs,
            retryAfter,
            requestId
          });
          await sleep(delay, opts.signal);
          continue;
        }
        const err = this.mapStatusToError(res.status, {
          message: serverMsg,
          path: desc.path,
          method: desc.method,
          requestId,
          body: parsed,
          retryAfter,
          rateLimit: rl,
          errBody
        });
        this.emit("onError", { ...evBase, statusCode: res.status, durationMs, requestId, error: err });
        throw err;
      } catch (e) {
        clearTimeout(timer);
        opts.signal?.removeEventListener("abort", onCallerAbort);
        if (e instanceof NavvoError) {
          lastError = e;
          throw e;
        }
        const isAbortError = e?.name === "AbortError" || e?.name === "TimeoutError";
        const aborted = isAbortError || controller.signal.aborted;
        if (aborted) {
          if (opts.signal?.aborted && !timedOut) {
            throw new NavvoAbortError("Request aborted by caller.", {
              path: desc.path,
              method: desc.method,
              redactedKey: this.config.redactedKey,
              cause: e
            });
          }
          lastError = e;
          if (attempt < maxAttempts - 1) {
            const delay = this.backoffDelay(attempt);
            this.emit("onRetry", { ...evBase, error: e });
            await sleep(delay, opts.signal);
            continue;
          }
          throw new NavvoTimeoutError(
            `Request to ${desc.path} timed out after ${maxAttempts} attempt(s).`,
            {
              path: desc.path,
              method: desc.method,
              redactedKey: this.config.redactedKey,
              attempts: maxAttempts,
              cause: e
            }
          );
        }
        lastError = e;
        if (attempt < maxAttempts - 1) {
          const delay = this.backoffDelay(attempt);
          this.emit("onRetry", { ...evBase, error: e });
          await sleep(delay, opts.signal);
          continue;
        }
        const netErr = new NavvoNetworkError(
          `Network error calling ${desc.path}: ${e?.message ?? String(e)}`,
          {
            path: desc.path,
            method: desc.method,
            redactedKey: this.config.redactedKey,
            cause: e
          }
        );
        this.emit("onError", { ...evBase, error: netErr });
        throw netErr;
      }
    }
    throw lastError instanceof NavvoError ? lastError : new NavvoNetworkError(`Request to ${desc.path} failed after all retries.`, {
      path: desc.path,
      method: desc.method,
      redactedKey: this.config.redactedKey,
      cause: lastError
    });
  }
  /** Map a non-2xx HTTP status to the appropriate typed error. */
  mapStatusToError(status, ctx) {
    const base = {
      status,
      path: ctx.path,
      method: ctx.method,
      requestId: ctx.requestId,
      redactedKey: this.config.redactedKey,
      body: ctx.body
    };
    const msg = ctx.message ?? `Navvo request failed with status ${status}.`;
    switch (status) {
      case 400:
      case 422:
        return new NavvoValidationError(msg, { ...base, fields: ctx.errBody.fields });
      case 401:
        return new NavvoAuthError(
          ctx.message ?? "Unauthorized \u2014 missing or unknown API key (401).",
          base
        );
      case 403:
        return new NavvoForbiddenError(
          ctx.message ?? "Forbidden \u2014 revoked key, missing scope, or quota exhausted (403).",
          { ...base, reason: ctx.errBody.reason ?? ctx.errBody.error, scope: ctx.errBody.scope }
        );
      case 404:
        return new NavvoNotFoundError(ctx.message ?? "Not found (404).", base);
      case 429:
        return new NavvoRateLimitError(
          ctx.message ?? "Rate limited (429).",
          {
            ...base,
            retryAfter: ctx.retryAfter,
            limit: ctx.rateLimit.limit,
            remaining: ctx.rateLimit.remaining,
            resetAt: ctx.rateLimit.resetAt
          }
        );
      case 502:
      case 503:
        return new NavvoServiceUnavailableError(msg, base);
      default:
        if (status >= 500) return new NavvoServerError(msg, base);
        return new NavvoError(msg, base);
    }
  }
};

// src/client.ts
var MAX_GEOCODE_LIMIT = 50;
var MAX_BATCH = 50;
function normalizeGeocodeResult(raw) {
  const lat = Number(raw.lat);
  const lng = Number(raw.lon ?? raw.lng);
  const bb = Array.isArray(raw.boundingbox) ? raw.boundingbox.map(Number) : void 0;
  return {
    placeId: raw.place_id != null ? Number(raw.place_id) : void 0,
    osmType: raw.osm_type,
    osmId: raw.osm_id != null ? Number(raw.osm_id) : void 0,
    lat,
    lng,
    name: raw.name,
    displayName: raw.display_name,
    category: raw.category,
    type: raw.type,
    address: raw.address,
    boundingBox: bb && bb.length === 4 ? [bb[0], bb[1], bb[2], bb[3]] : void 0,
    raw
  };
}
var NavvoClient = class _NavvoClient {
  /** The resolved, frozen configuration. */
  config;
  transport;
  constructor(options = {}) {
    this.config = resolveConfig(options);
    this.transport = new HttpTransport(this.config);
  }
  /** Construct a client reading `NAVVO_API_KEY` from the environment. */
  static fromEnv(options = {}) {
    return new _NavvoClient(options);
  }
  /** A clone of this client that uses a different API key (multi-tenant servers). */
  withApiKey(apiKey) {
    return new _NavvoClient({ ...this.optionsForClone(), apiKey });
  }
  optionsForClone() {
    return {
      baseUrl: this.config.baseUrl,
      timeoutMs: this.config.timeoutMs,
      retry: this.config.retry,
      defaultHeaders: this.config.defaultHeaders,
      telemetry: this.config.telemetry,
      fetch: this.config.fetch
      // userAgent already baked into config.userAgent; reuse it verbatim.
    };
  }
  // ======================================================================
  // Static / coordinate helpers
  // ======================================================================
  /** Parse a place's WKT `center` (`POINT(lng lat)`) into a {@link LatLng}. */
  static parseCenter(wkt) {
    return parseWktPoint(wkt);
  }
  /** Decode an encoded polyline (polyline6 by default) into `LatLng[]`. */
  static decodePolyline(encoded, precision = 6) {
    return decodePolyline(encoded, precision);
  }
  // ======================================================================
  // Tiles / styles / map config
  // ======================================================================
  /** List the available map styles. `GET /tiles/styles`. */
  styles(opts) {
    return this.transport.request({ method: "GET", path: "/tiles/styles" }, opts);
  }
  /**
   * Build a browser-fetchable style URL with the key as a query param.
   *
   * Map tiles/styles are fetched by the browser and can't set headers, so they
   * use `?api_key=` (this is the **only** place the key goes in a URL).
   */
  styleUrl(style = "standard") {
    const u = new URL(`${this.config.baseUrl}/tiles/style`);
    u.searchParams.set("style", style);
    u.searchParams.set("api_key", this.config.apiKey);
    return u.toString();
  }
  /** Fetch the runtime map configuration. `GET /map/config`. */
  mapConfig(opts) {
    return this.transport.request({ method: "GET", path: "/map/config" }, opts);
  }
  // ======================================================================
  // Search
  // ======================================================================
  /** Full-text place search. `GET /search`. Scope `search`. */
  search(q, options = {}, opts) {
    if (!q || !q.trim()) throw new NavvoConfigError("search: `q` must be a non-empty string.");
    return this.transport.request(
      {
        method: "GET",
        path: "/search",
        query: {
          q,
          locale: options.locale ?? options.lang,
          limit: options.limit,
          page: options.page,
          bbox: options.bbox ? bboxToString(options.bbox) : void 0
        }
      },
      opts
    );
  }
  // ======================================================================
  // Geocoding
  // ======================================================================
  /** Forward geocode a free-text query. `GET /geocode/search`. Scope `geocode`. */
  async geocode(q, options = {}, opts) {
    if (!q || !q.trim()) throw new NavvoConfigError("geocode: `q` must be a non-empty string.");
    let limit = options.limit;
    if (limit !== void 0 && limit > MAX_GEOCODE_LIMIT) {
      limit = MAX_GEOCODE_LIMIT;
    }
    const query = { q, locale: options.locale, limit };
    if (options.near) {
      assertLatLng(options.near);
      query.lat = options.near.lat;
      query.lon = options.near.lng;
    }
    if (options.bbox) query.bbox = bboxToString(options.bbox);
    const body = await this.transport.request({ method: "GET", path: "/geocode/search", query }, opts);
    const data = Array.isArray(body?.data) ? body.data : [];
    return data.map((d) => normalizeGeocodeResult(d));
  }
  /** Reverse geocode a coordinate to the nearest address. `GET /geocode/reverse`. */
  async reverseGeocode(lat, lon, opts) {
    assertLatLng({ lat, lng: lon });
    const body = await this.transport.request({ method: "GET", path: "/geocode/reverse", query: { lat, lon } }, opts);
    if (!body?.data || typeof body.data !== "object") return null;
    return normalizeGeocodeResult(body.data);
  }
  /**
   * Bulk forward geocode. `POST /geocode/batch`. Auto-chunks inputs into
   * 50-query requests and stitches the results.
   */
  async geocodeBatch(items, options = {}, opts) {
    if (!Array.isArray(items) || items.length === 0) {
      throw new NavvoConfigError("geocodeBatch: `items` must be a non-empty array.");
    }
    const out = [];
    for (let i = 0; i < items.length; i += MAX_BATCH) {
      const chunk = items.slice(i, i + MAX_BATCH);
      const body = await this.transport.request(
        {
          method: "POST",
          path: "/geocode/batch",
          body: {
            queries: chunk,
            locale: options.locale,
            bbox: options.bbox ? bboxToString(options.bbox) : void 0
          }
        },
        opts
      );
      if (Array.isArray(body?.results)) out.push(...body.results);
    }
    return out;
  }
  /**
   * Bulk reverse geocode. `POST /geocode/reverse-batch`. Auto-chunks into
   * 50-point requests.
   */
  async reverseGeocodeBatch(points, opts) {
    if (!Array.isArray(points) || points.length === 0) {
      throw new NavvoConfigError("reverseGeocodeBatch: `points` must be a non-empty array.");
    }
    points.forEach(assertLatLng);
    const out = [];
    for (let i = 0; i < points.length; i += MAX_BATCH) {
      const chunk = points.slice(i, i + MAX_BATCH).map((p) => ({ lat: p.lat, lng: p.lng }));
      const body = await this.transport.request(
        { method: "POST", path: "/geocode/reverse-batch", body: { points: chunk } },
        opts
      );
      if (Array.isArray(body?.results)) out.push(...body.results);
    }
    return out;
  }
  // ======================================================================
  // Places
  // ======================================================================
  /** Paginated, sponsored-first place listing. `GET /places`. Scope `places`. */
  list(options = {}, opts) {
    const query = {
      q: options.q,
      page: options.page,
      limit: options.limit,
      category: options.category,
      amenities: options.amenities,
      openNow: options.openNow,
      sort: options.sort,
      minRating: options.minRating,
      radiusKm: options.radiusKm,
      sponsored: options.sponsored
    };
    if (options.near) {
      assertLatLng(options.near);
      query.near = `${options.near.lat},${options.near.lng}`;
    }
    return this.transport.request({ method: "GET", path: "/places", query }, opts);
  }
  /**
   * Async-iterate every place across all pages. Stops when
   * `page*limit >= total`. Errors surface mid-iteration.
   *
   * ```ts
   * for await (const place of navvo.iterate({ category: "restaurants", limit: 100 })) {
   *   await sink(place);
   * }
   * ```
   */
  async *iterate(options = {}, opts) {
    let page = options.page ?? 1;
    const limit = options.limit ?? 50;
    while (true) {
      const res = await this.list({ ...options, page, limit }, opts);
      for (const item of res.items ?? []) yield item;
      const total = res.total ?? 0;
      if (!res.items?.length || page * limit >= total) break;
      page += 1;
    }
  }
  /** Lightweight thumbnail + open-state for a place. `GET /places/thumb`. */
  async thumb(slug, opts) {
    if (!slug) throw new NavvoConfigError("thumb: `slug` is required.");
    const body = await this.transport.request(
      { method: "GET", path: "/places/thumb", query: { slug } },
      opts
    );
    if (!body || body.url === null && body.openState === null && body.rating === null) {
      return body ?? null;
    }
    return body;
  }
  /** Amenity facet groups (for a filter UI). `GET /places/facets`. */
  facets(options = {}, opts) {
    return this.transport.request(
      { method: "GET", path: "/places/facets", query: { q: options.q } },
      opts
    );
  }
  /** The full category taxonomy tree. `GET /places/categories`. */
  placeCategories(opts) {
    return this.transport.request(
      { method: "GET", path: "/places/categories" },
      opts
    );
  }
  /**
   * Full place detail (reviews, photos, Q&A, arrival intelligence).
   * `GET /places/{slug}`.
   *
   * Soft-null getter: an unknown slug yields a genuine HTTP 404 from the API,
   * which this method catches ({@link NavvoNotFoundError}) and returns as
   * `null` — it does **not** throw on not-found:
   *
   * ```ts
   * const place = await navvo.place(slug); // PlaceDetail, or null if unknown
   * if (!place) return; // not found
   * ```
   *
   * (Other errors — auth, rate-limit, validation, outage — still throw.)
   */
  async place(slug, opts) {
    if (!slug) throw new NavvoConfigError("place: `slug` is required.");
    try {
      const body = await this.transport.request(
        { method: "GET", path: `/places/${encodeURIComponent(slug)}` },
        opts
      );
      return body ?? null;
    } catch (e) {
      if (e instanceof NavvoNotFoundError) return null;
      throw e;
    }
  }
  /** The recommended entrance to route to for a place detail, else its center. */
  static routeTargetFor(place) {
    if (!place) return null;
    const cand = place.arrivalIntelligence?.candidates?.[0]?.routeTarget;
    if (cand && typeof cand.lat === "number" && typeof cand.lng === "number") {
      return { lat: cand.lat, lng: cand.lng };
    }
    return parseWktPoint(place.center);
  }
  /** Opening-hours engine (holiday-aware). `GET /places/{slug}/hours`. */
  hours(slug, options = {}, opts) {
    if (!slug) throw new NavvoConfigError("hours: `slug` is required.");
    return this.transport.request(
      { method: "GET", path: `/places/${encodeURIComponent(slug)}/hours`, query: { at: options.at } },
      opts
    );
  }
  /** Busyness grid. `GET /places/{slug}/popular-times`. */
  popularTimes(slug, opts) {
    if (!slug) throw new NavvoConfigError("popularTimes: `slug` is required.");
    return this.transport.request(
      { method: "GET", path: `/places/${encodeURIComponent(slug)}/popular-times` },
      opts
    );
  }
  /** Structured About attributes. `GET /places/{slug}/attributes`. */
  attributes(slug, opts) {
    if (!slug) throw new NavvoConfigError("attributes: `slug` is required.");
    return this.transport.request(
      { method: "GET", path: `/places/${encodeURIComponent(slug)}/attributes` },
      opts
    );
  }
  /** Rating distribution. `GET /places/{slug}/reviews-summary`. */
  reviewsSummary(slug, opts) {
    if (!slug) throw new NavvoConfigError("reviewsSummary: `slug` is required.");
    return this.transport.request(
      { method: "GET", path: `/places/${encodeURIComponent(slug)}/reviews-summary` },
      opts
    );
  }
  /** Paginated photo gallery (featured first). `GET /places/{slug}/photos`. */
  photos(slug, options = {}, opts) {
    if (!slug) throw new NavvoConfigError("photos: `slug` is required.");
    return this.transport.request(
      {
        method: "GET",
        path: `/places/${encodeURIComponent(slug)}/photos`,
        query: { page: options.page, limit: options.limit }
      },
      opts
    );
  }
  /** "More like this" nearby places. `GET /places/{slug}/nearby`. */
  nearby(slug, options = {}, opts) {
    if (!slug) throw new NavvoConfigError("nearby: `slug` is required.");
    return this.transport.request(
      {
        method: "GET",
        path: `/places/${encodeURIComponent(slug)}/nearby`,
        query: {
          radiusKm: options.radiusKm,
          sameCategory: options.sameCategory ? 1 : void 0,
          limit: options.limit
        }
      },
      opts
    );
  }
  /** Isochrone-bounded "what's reachable". `POST /places/reachable`. */
  reachable(req, opts) {
    assertLatLng(req.origin);
    return this.transport.request(
      {
        method: "POST",
        path: "/places/reachable",
        body: {
          origin: toLatLngObj(req.origin),
          minutes: req.minutes,
          costing: req.costing,
          q: req.q,
          categories: req.categories?.join(","),
          amenities: req.amenities?.join(","),
          limit: req.limit
        }
      },
      opts
    );
  }
  // ======================================================================
  // Routing
  // ======================================================================
  /** High-level corridor-aware route planner. `POST /routing/plan` (HTTP 201). */
  route(req, opts) {
    if (!Array.isArray(req.locations) || req.locations.length < 2) {
      throw new NavvoConfigError("route: at least 2 locations are required.");
    }
    req.locations.forEach(assertLatLng);
    const body = {
      locations: req.locations.map(toLonLat),
      costing: req.costing,
      preference: req.preference,
      avoid: req.avoid,
      optimize: req.optimize,
      avoidIncidents: req.avoidIncidents,
      avoidReported: req.avoidReported,
      disregard: req.disregard,
      constraintsText: req.constraintsText,
      includeLabels: req.includeLabels,
      language: req.language,
      vehicle: req.vehicle ?? req.truck,
      departureTime: req.departureTime
    };
    return this.transport.request(
      { method: "POST", path: "/routing/plan", body, okStatuses: [200, 201] },
      opts
    );
  }
  /** Low-level passthrough directions. `POST /routing/route` (HTTP 201). */
  simpleRoute(req, opts) {
    if (!Array.isArray(req.locations) || req.locations.length < 2) {
      throw new NavvoConfigError("simpleRoute: at least 2 locations are required.");
    }
    req.locations.forEach(assertLatLng);
    const { locations, costing, units, language, alternates, ...rest } = req;
    const payload = {
      locations: locations.map(toLatLon),
      ...costing ? { costing } : {},
      ...units || language ? { directions_options: { units, language } } : {},
      ...alternates !== void 0 ? { alternates } : {},
      ...rest
    };
    return this.transport.request(
      { method: "POST", path: "/routing/route", body: { payload }, okStatuses: [200, 201] },
      opts
    );
  }
  /** Travel-time / distance matrix. `POST /routing/matrix` (HTTP 201). */
  matrix(req, opts) {
    req.sources.forEach(assertLatLng);
    req.targets.forEach(assertLatLng);
    const { sources, targets, costing, units, ...rest } = req;
    const payload = {
      sources: sources.map(toLatLon),
      targets: targets.map(toLatLon),
      ...costing ? { costing } : {},
      ...units ? { units } : {},
      ...rest
    };
    return this.transport.request(
      { method: "POST", path: "/routing/matrix", body: { payload }, okStatuses: [200, 201] },
      opts
    );
  }
  /** Reachability polygons. `POST /routing/isochrone` (HTTP 201). */
  isochrone(req, opts) {
    req.locations.forEach(assertLatLng);
    const payload = {
      locations: req.locations.map(toLatLon),
      ...req.costing ? { costing: req.costing } : {},
      contours: req.contours.map((c) => {
        const out = {};
        if (c.minutes !== void 0) out.time = c.minutes;
        if (c.km !== void 0) out.distance = c.km;
        if (c.color !== void 0) out.color = c.color;
        return out;
      }),
      ...req.polygons !== void 0 ? { polygons: req.polygons } : {}
    };
    return this.transport.request(
      { method: "POST", path: "/routing/isochrone", body: { payload }, okStatuses: [200, 201] },
      opts
    );
  }
  /** Snap a GPS trace to roads. `POST /routing/map-match` (HTTP 201). */
  mapMatch(req, opts) {
    const { shape, encodedPolyline, costing, shapeMatch, ...rest } = req;
    const payload = { ...rest };
    if (shape) payload.shape = shape.map((p) => ({ lat: p.lat, lon: p.lng, ...p.time !== void 0 ? { time: p.time } : {} }));
    if (encodedPolyline) payload.encoded_polyline = encodedPolyline;
    if (costing) payload.costing = costing;
    if (shapeMatch) payload.shape_match = shapeMatch;
    return this.transport.request(
      { method: "POST", path: "/routing/map-match", body: { payload }, okStatuses: [200, 201] },
      opts
    );
  }
  /** TSP stop ordering via the routing engine. `POST /routing/optimize` (HTTP 201). */
  routingOptimize(req, opts) {
    req.locations.forEach(assertLatLng);
    const { locations, costing, ...rest } = req;
    const payload = {
      locations: locations.map(toLatLon),
      ...costing ? { costing } : {},
      ...rest
    };
    return this.transport.request(
      { method: "POST", path: "/routing/optimize", body: { payload }, okStatuses: [200, 201] },
      opts
    );
  }
  /** The routing-profile catalog. `GET /routing/profiles`. */
  routingProfiles(opts) {
    return this.transport.request(
      { method: "GET", path: "/routing/profiles" },
      opts
    );
  }
  // ======================================================================
  // Optimization (OR-Tools VRP) — scope optimize
  // ======================================================================
  /** Optimizer liveness probe. `GET /optimize/health`. */
  optimizeHealth(opts) {
    return this.transport.request(
      { method: "GET", path: "/optimize/health" },
      opts
    );
  }
  /** Core VRP/VRPTW solver. `POST /optimize/routes`. Scope `optimize`. */
  optimize(req, opts) {
    if (!Array.isArray(req.stops) || req.stops.length < 1) {
      throw new NavvoConfigError("optimize: `stops` must include at least the depot.");
    }
    req.stops.forEach(assertLatLng);
    const effectiveOpts = this.tierTimeout(req.tier, opts);
    return this.transport.request(
      {
        method: "POST",
        path: "/optimize/routes",
        body: { ...req, stops: req.stops.map(toLatLngObj) }
      },
      effectiveOpts
    );
  }
  /** Mid-day re-plan from current positions. `POST /optimize/reoptimize`. */
  reoptimize(req, opts) {
    req.vehiclePositions.forEach(assertLatLng);
    req.remainingStops.forEach(assertLatLng);
    return this.transport.request(
      {
        method: "POST",
        path: "/optimize/reoptimize",
        body: {
          ...req,
          vehiclePositions: req.vehiclePositions.map(toLatLngObj),
          remainingStops: req.remainingStops.map(toLatLngObj)
        }
      },
      this.tierTimeout(req.tier, opts)
    );
  }
  /** CP-SAT bin packing. `POST /optimize/binpack`. */
  binpack(req, opts) {
    return this.transport.request(
      { method: "POST", path: "/optimize/binpack", body: req },
      opts
    );
  }
  /** CP-SAT driver/shift rostering. `POST /optimize/roster`. */
  roster(req, opts) {
    return this.transport.request(
      { method: "POST", path: "/optimize/roster", body: req },
      opts
    );
  }
  /** Enqueue an optimization as an async job. `POST /optimize/jobs`. */
  submitJob(kind, req, opts) {
    return this.transport.request(
      { method: "POST", path: "/optimize/jobs", body: { kind, ...req } },
      opts
    );
  }
  /** Poll an optimization job once. `GET /optimize/jobs/:id`. */
  job(id, opts) {
    if (!id) throw new NavvoConfigError("job: `id` is required.");
    return this.transport.request(
      { method: "GET", path: `/optimize/jobs/${encodeURIComponent(id)}` },
      opts
    );
  }
  /**
   * Poll a job until it is `completed`/`failed` (or the timeout elapses).
   * Throws on a `failed` job or on timeout.
   */
  async waitForJob(id, options = {}) {
    const timeoutMs = options.timeoutMs ?? 12e4;
    const pollMs = options.pollMs ?? 1500;
    const deadline = Date.now() + timeoutMs;
    while (true) {
      const snapshot = await this.job(id, { signal: options.signal });
      if (snapshot.status === "completed") return snapshot;
      if (snapshot.status === "failed") {
        throw new NavvoServiceUnavailableError(
          `Optimization job ${id} failed: ${snapshot.error ?? "unknown error"}.`,
          { path: `/optimize/jobs/${id}`, redactedKey: this.config.redactedKey, body: snapshot }
        );
      }
      if (snapshot.status === "unknown") {
        throw new NavvoServiceUnavailableError(`Optimization job ${id} not found.`, {
          path: `/optimize/jobs/${id}`,
          redactedKey: this.config.redactedKey,
          body: snapshot
        });
      }
      if (Date.now() + pollMs > deadline) {
        throw new NavvoServiceUnavailableError(
          `Timed out waiting ${timeoutMs}ms for optimization job ${id}.`,
          { path: `/optimize/jobs/${id}`, redactedKey: this.config.redactedKey }
        );
      }
      await new Promise((resolve, reject) => {
        const t = setTimeout(resolve, pollMs);
        options.signal?.addEventListener(
          "abort",
          () => {
            clearTimeout(t);
            reject(new NavvoConfigError("waitForJob aborted by caller."));
          },
          { once: true }
        );
      });
    }
  }
  tierTimeout(tier, opts) {
    if (tier === "best" && (!opts || opts.timeoutMs === void 0)) {
      return { ...opts, timeoutMs: Math.max(this.config.timeoutMs, 3e4) };
    }
    return opts;
  }
  // ======================================================================
  // Traffic — scope routing + traffic
  // ======================================================================
  /** Traffic-aware ETA + congestion segments. `POST /traffic/route`. Scopes routing+traffic. */
  trafficRoute(req, opts) {
    if (!Array.isArray(req.locations) || req.locations.length < 2) {
      throw new NavvoConfigError("trafficRoute: at least 2 locations are required.");
    }
    req.locations.forEach(assertLatLng);
    const { locations, ...rest } = req;
    return this.transport.request(
      {
        method: "POST",
        path: "/traffic/route",
        body: { locations: locations.map(toLonLat), ...rest },
        okStatuses: [200, 201]
      },
      opts
    );
  }
  /** Traffic summary for a bbox. `GET /traffic/summary`. Scope `traffic`. */
  trafficSummary(bbox, opts) {
    return this.transport.request(
      { method: "GET", path: "/traffic/summary", query: { bbox: bboxToString(bbox) } },
      opts
    );
  }
  // ======================================================================
  // Incidents / road edits / risk
  // ======================================================================
  /** Active incidents as GeoJSON. `GET /incidents/geojson`. */
  incidents(options = {}, opts) {
    return this.transport.request(
      {
        method: "GET",
        path: "/incidents/geojson",
        query: {
          bbox: options.bbox ? bboxToString(options.bbox) : void 0,
          type: options.type,
          limit: options.limit
        }
      },
      opts
    );
  }
  /** The catalog of incident types. `GET /incidents/types`. */
  incidentTypes(opts) {
    return this.transport.request({ method: "GET", path: "/incidents/types" }, opts);
  }
  /** Report a new incident. `POST /incidents/report`. */
  reportIncident(req, opts) {
    assertLatLng({ lat: req.lat, lng: req.lng });
    return this.transport.request(
      { method: "POST", path: "/incidents/report", body: req, okStatuses: [200, 201] },
      opts
    );
  }
  /** Platform road edits as GeoJSON. `GET /road-edits/geojson`. */
  roadEdits(bbox, opts) {
    return this.transport.request(
      { method: "GET", path: "/road-edits/geojson", query: { bbox: bboxToString(bbox) } },
      opts
    );
  }
  /** Logistics-zone risk for a route geometry. `POST /map/route-risk`. */
  routeRisk(geometry, opts) {
    return this.transport.request(
      { method: "POST", path: "/map/route-risk", body: { geometry }, okStatuses: [200, 201] },
      opts
    );
  }
  // ======================================================================
  // Weather / air quality
  // ======================================================================
  /** Current weather at a point. `GET /map/weather/point`. */
  weatherPoint(lat, lng, options = {}, opts) {
    assertLatLng({ lat, lng });
    return this.transport.request(
      { method: "GET", path: "/map/weather/point", query: { lat, lng, units: options.units } },
      opts
    );
  }
  /** Multi-day forecast at a point. `GET /map/weather/forecast`. */
  weatherForecast(lat, lng, opts) {
    assertLatLng({ lat, lng });
    return this.transport.request(
      { method: "GET", path: "/map/weather/forecast", query: { lat, lng } },
      opts
    );
  }
  /** City weather pins in a bbox. `GET /map/weather/cities`. */
  weatherCities(bbox, zoom, opts) {
    return this.transport.request(
      { method: "GET", path: "/map/weather/cities", query: { bbox: bboxToString(bbox), zoom } },
      opts
    );
  }
  /** City air-quality pins in a bbox. `GET /map/airquality/cities`. */
  airQualityCities(bbox, zoom, opts) {
    return this.transport.request(
      { method: "GET", path: "/map/airquality/cities", query: { bbox: bboxToString(bbox), zoom } },
      opts
    );
  }
  // ======================================================================
  // EV / transit
  // ======================================================================
  /** EV charging stations. `GET /ev/stations`. */
  evStations(options = {}, opts) {
    return this.transport.request(
      {
        method: "GET",
        path: "/ev/stations",
        query: {
          bbox: options.bbox ? bboxToString(options.bbox) : void 0,
          limit: options.limit
        }
      },
      opts
    );
  }
  /** Nearby transit stops. `GET /transit/stops`. */
  transitStops(lat, lng, radius, opts) {
    assertLatLng({ lat, lng });
    return this.transport.request(
      { method: "GET", path: "/transit/stops", query: { lat, lng, radius } },
      opts
    );
  }
  /** GBFS shared-mobility feed near a point. `GET /transit/gbfs`. */
  gbfs(lat, lng, radius, opts) {
    assertLatLng({ lat, lng });
    return this.transport.request(
      { method: "GET", path: "/transit/gbfs", query: { lat, lng, radius } },
      opts
    );
  }
};

exports.API_KEY_PATTERN = API_KEY_PATTERN;
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
exports.NavvoAbortError = NavvoAbortError;
exports.NavvoAuthError = NavvoAuthError;
exports.NavvoClient = NavvoClient;
exports.NavvoConfigError = NavvoConfigError;
exports.NavvoError = NavvoError;
exports.NavvoForbiddenError = NavvoForbiddenError;
exports.NavvoNetworkError = NavvoNetworkError;
exports.NavvoNotFoundError = NavvoNotFoundError;
exports.NavvoRateLimitError = NavvoRateLimitError;
exports.NavvoServerError = NavvoServerError;
exports.NavvoServiceUnavailableError = NavvoServiceUnavailableError;
exports.NavvoTimeoutError = NavvoTimeoutError;
exports.NavvoValidationError = NavvoValidationError;
exports.SDK_VERSION = SDK_VERSION;
exports.assertLatLng = assertLatLng;
exports.bboxToString = bboxToString;
exports.buildUrl = buildUrl;
exports.decodePolyline = decodePolyline;
exports.fromGeoJSON = fromGeoJSON;
exports.parseWktPoint = parseWktPoint;
exports.redactKey = redactKey;
exports.resolveConfig = resolveConfig;
exports.toGeoJSON = toGeoJSON;
//# sourceMappingURL=index.cjs.map
//# sourceMappingURL=index.cjs.map