SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
4 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
22.8 KB · 386 lines typescript
Raw Blame History
1/**2 * /map data: zoom-tiered clusters / points for facilities, thematic layers (capacity, projects, AI, cloud, IXPs,3 * connectivity, power, pipeline), density grids and a "time machine" year filter. Only entities with coordinates are4 * drawn; IXPs without a published location fall back to their metro reference point (p = "metro"). Layers without a5 * connector (landing stations, substations, power plants) are never drawn — power events are exposed as metro-level6 * clusters, not as invented plant locations.7 */8import type { DensityCell, DensityView, FacilityStatus, MapCluster, MapLayer, MapPoint, MapResponse } from "@dci/core";9import { pg, andAll, facilityJoins, facilityView, knownMwAgg, pipelineMwAgg, countedAgg, mwExpr, projectLive, yearExpr, PIPELINE_SET, POWER_EVENT_TYPES, GRID_EVENT_TYPES, type Fragment, type Sql } from "../lib/sql.js";10import { facilityConds, type FacilityQuery } from "./facilities.js";11import { int, num, str, type Row } from "../lib/rows.js";12import { asPrecision, asStatus, asType } from "../lib/dto.js";1314export interface Bbox { w: number; s: number; e: number; n: number }1516export interface MapQuery {17  zoom: number;18  bbox?: Bbox;19  status?: string[];20  type?: string[];21  operator?: string;22  metro?: string;23  country?: string[];24  q?: string;25  min_mw?: number;26  max_mw?: number;27  ai?: boolean;28  hyperscale?: boolean;29  has_mw?: boolean;30  confidence?: string[];31  opened_from?: number;32  opened_to?: number;33  cloud_regions?: boolean;34  layer?: MapLayer;35  density?: DensityView;36  year?: number;37  project_status?: string[];38  expected_from?: number;39  expected_to?: number;40  location_precision?: string[];41  ai_evidence?: string[];42}4344export const MAX_POINTS = 5000;45export const MAP_METHODOLOGY = "Only entities with coordinates are drawn; `p` is the geo precision (city/metro-level points are approximate). Facility `mw` = best known figure (IT, else total, else planned) — on the power layer it is utility / grid supply capacity, NOT IT load. `k` marks overlay kinds (project, ixp, cloud_region); IXPs without a published location sit at their metro reference point. Density weights are containment-aware (a campus and its buildings are never both summed). `year` keeps facilities whose opening date is ≤ year; `yearCoverage` is the share of matching facilities that have an opening date at all — undated facilities are absent from every year frame.";4647/** Map mode from zoom: <5 countries, 5–8 grid, ≥9 points. Exported for tests. */48export function mapMode(zoom: number): "country" | "grid" | "points" {49  if (zoom < 5) return "country";50  if (zoom < 9) return "grid";51  return "points";52}5354export function cellSize(zoom: number): number {55  return 360 / Math.pow(2, Math.max(0, Math.min(zoom, 12)) + 1);56}5758function bboxCond(sql: Sql, b: Bbox | undefined, latCol: Fragment, lngCol: Fragment): Fragment {59  if (!b) return sql`true`;60  const lng = b.w <= b.e ? sql`${lngCol} between ${b.w} and ${b.e}` : sql`(${lngCol} >= ${b.w} or ${lngCol} <= ${b.e})`;61  return sql`(${latCol} between ${b.s} and ${b.n} and ${lng})`;62}6364const AI_COND = (sql: Sql) => sql`(f.ai_evidence in ('confirmed', 'likely') or f.is_ai)`;65const PIPELINE_PROJECT_STATUSES = ["rumored", "proposed", "announced", "permitting", "approved", "under_construction", "delayed"];6667/** Layer-specific facility conditions (on top of the generic filters). */68function layerConds(sql: Sql, q: MapQuery): Fragment[] {69  const out: Fragment[] = [];70  switch (q.layer) {71    case "capacity": out.push(sql`${mwExpr(sql)} is not null`); break;72    case "ai": out.push(AI_COND(sql)); break;73    case "power": out.push(sql`(f.utility_capacity_mw is not null or f.grid_connection_mw is not null)`); break;74    case "pipeline": out.push(sql`f.status = any(${PIPELINE_SET})`); break;75    case "connectivity": out.push(sql`(f.facility_type = 'carrier_hotel' or coalesce(f.carriers_count, 0) >= 20)`); break;76    default: break;77  }78  if (q.location_precision?.length) out.push(sql`f.geo_precision = any(${q.location_precision})`);79  if (q.ai_evidence?.length) out.push(sql`f.ai_evidence = any(${q.ai_evidence})`);80  return out;81}8283/** Time-machine condition: opened_on year ≤ year; pipeline rows only when the caller asked for pipeline statuses. */84function yearCond(sql: Sql, q: MapQuery): Fragment {85  if (q.year == null) return sql`true`;86  const opened = sql`${yearExpr(sql, sql`f.opened_on`)} <= ${q.year}`;87  const pipelineAsked = (q.status ?? []).some((s) => (PIPELINE_SET as string[]).includes(s));88  if (!pipelineAsked) return opened;89  return sql`(${opened} or (f.status = any(${PIPELINE_SET}) and ${yearExpr(sql, sql`coalesce(f.announced_on, f.construction_started_on, f.opened_on)`)} <= ${q.year}))`;90}9192function baseConds(sql: Sql, q: MapQuery): Fragment[] {93  const fq: FacilityQuery = { status: q.status, type: q.type, operator: q.operator, metro: q.metro, country: q.country, q: q.q, min_mw: q.min_mw, max_mw: q.max_mw, ai: q.ai, hyperscale: q.hyperscale, has_mw: q.has_mw, confidence: q.confidence, opened_from: q.opened_from, opened_to: q.opened_to };94  return [...facilityConds(sql, fq), ...layerConds(sql, q), sql`f.lat is not null and f.lng is not null`, bboxCond(sql, q.bbox, sql`f.lat`, sql`f.lng`)];95}9697function conds(sql: Sql, q: MapQuery): Fragment {98  return andAll(sql, [...baseConds(sql, q), yearCond(sql, q)]);99}100101/** Layers whose facility points are hidden (overlay-only layers). */102function facilitiesDrawn(q: MapQuery): boolean {103  return q.layer !== "cloud" && q.layer !== "ixps" && q.layer !== "projects";104}105106function addStatus(target: Partial<Record<FacilityStatus, number>>, status: unknown, n: number): void {107  const s = asStatus(status);108  target[s] = (target[s] ?? 0) + n;109}110111/** MW column drawn for the layer: power → utility/grid supply, else the best known figure. */112function pointMw(sql: Sql, q: MapQuery): Fragment {113  return q.layer === "power" ? sql`coalesce(f.utility_capacity_mw, f.grid_connection_mw)` : mwExpr(sql);114}115116async function countryClusters(sql: Sql, q: MapQuery): Promise<MapCluster[]> {117  const mw = pointMw(sql, q);118  const rows = await sql<Row[]>`119    select f.country_iso2, f.status, count(*)::int as n, sum(${mw})::float as mw, sum(f.lat)::float as slat, sum(f.lng)::float as slng, c.lat as clat, c.lng as clng, c.name as cname120    from facilities f ${facilityJoins(sql)}121    where ${conds(sql, q)}122    group by f.country_iso2, f.status, c.lat, c.lng, c.name`;123  const by = new Map<string, MapCluster & { _slat: number; _slng: number; _hasMw: boolean }>();124  for (const r of rows) {125    const key = str(r.country_iso2) ?? "unknown";126    let c = by.get(key);127    if (!c) { c = { key, lat: 0, lng: 0, count: 0, mw: null, statuses: {}, label: str(r.cname) ?? (key === "unknown" ? null : key), _slat: 0, _slng: 0, _hasMw: false }; by.set(key, c); }128    const n = int(r.n);129    c.count += n;130    c._slat += num(r.slat) ?? 0;131    c._slng += num(r.slng) ?? 0;132    const m = num(r.mw);133    if (m != null) { c.mw = (c.mw ?? 0) + m; c._hasMw = true; }134    addStatus(c.statuses, r.status, n);135    const clat = num(r.clat), clng = num(r.clng);136    if (clat != null && clng != null) { c.lat = clat; c.lng = clng; }137  }138  // Facilities without a resolved country must not collapse into one meaningless blob at their mean coordinate:139  // re-cluster them on a coarse grid (zoom-4 cells) instead.140  const unknown = by.get("unknown");141  if (unknown) {142    by.delete("unknown");143    const size = cellSize(4);144    const cells = await sql<Row[]>`145      select floor((f.lng + 180) / ${size})::int as i, floor((f.lat + 90) / ${size})::int as j, f.status, count(*)::int as n, sum(${mw})::float as mw, avg(f.lat)::float as alat, avg(f.lng)::float as alng146      from facilities f ${facilityJoins(sql)}147      where ${conds(sql, q)} and f.country_iso2 is null148      group by 1, 2, 3`;149    for (const r of cells) {150      const key = `cell:${int(r.i)}:${int(r.j)}`;151      let c = by.get(key);152      if (!c) { c = { key, lat: num(r.alat) ?? 0, lng: num(r.alng) ?? 0, count: 0, mw: null, statuses: {}, label: null, _slat: 0, _slng: 0, _hasMw: false }; by.set(key, c); }153      const n = int(r.n);154      c.count += n;155      const m = num(r.mw);156      if (m != null) { c.mw = (c.mw ?? 0) + m; c._hasMw = true; }157      addStatus(c.statuses, r.status, n);158    }159  }160  return [...by.values()].map((c) => {161    const { _slat, _slng, _hasMw, ...rest } = c;162    if (!rest.lat && !rest.lng && rest.count) { rest.lat = _slat / rest.count; rest.lng = _slng / rest.count; }163    rest.lat = Math.round(rest.lat * 1e4) / 1e4; rest.lng = Math.round(rest.lng * 1e4) / 1e4;164    if (rest.mw != null) rest.mw = Math.round(rest.mw * 100) / 100;165    return rest;166  }).sort((a, b) => b.count - a.count);167}168169async function gridClusters(sql: Sql, q: MapQuery, zoom: number): Promise<MapCluster[]> {170  const size = cellSize(zoom);171  const mw = pointMw(sql, q);172  const rows = await sql<Row[]>`173    select floor((f.lng + 180) / ${size})::int as i, floor((f.lat + 90) / ${size})::int as j, f.status, count(*)::int as n, sum(${mw})::float as mw174    from facilities f ${facilityJoins(sql)}175    where ${conds(sql, q)}176    group by 1, 2, 3`;177  const by = new Map<string, MapCluster>();178  for (const r of rows) {179    const i = int(r.i), j = int(r.j);180    const key = `${zoom}:${i}:${j}`;181    let c = by.get(key);182    if (!c) { c = { key, lat: Math.round((j * size - 90 + size / 2) * 1e4) / 1e4, lng: Math.round((i * size - 180 + size / 2) * 1e4) / 1e4, count: 0, mw: null, statuses: {}, label: null }; by.set(key, c); }183    const n = int(r.n);184    c.count += n;185    const m = num(r.mw);186    if (m != null) c.mw = Math.round(((c.mw ?? 0) + m) * 100) / 100;187    addStatus(c.statuses, r.status, n);188  }189  return [...by.values()].sort((a, b) => b.count - a.count);190}191192async function points(sql: Sql, q: MapQuery): Promise<MapPoint[] | null> {193  const mw = pointMw(sql, q);194  const rows = await sql<Row[]>`195    select f.id, f.slug, f.name, o.name as op_name, f.lat, f.lng, f.status, f.facility_type, ${mw} as mw, f.geo_precision, f.is_ai, f.ai_evidence, f.is_hyperscale, f.country_iso2, f.record_scope, ${yearExpr(sql, sql`f.opened_on`)} as y196    from facilities f ${facilityJoins(sql)}197    where ${conds(sql, q)}198    order by ${mw} desc nulls last, f.id199    limit ${MAX_POINTS + 1}`;200  if (rows.length > MAX_POINTS) return null;201  return rows.map((r) => {202    const p: MapPoint = { id: String(r.id), slug: String(r.slug), n: String(r.name), o: str(r.op_name), lat: num(r.lat) ?? 0, lng: num(r.lng) ?? 0, s: asStatus(r.status), t: asType(r.facility_type), mw: num(r.mw), p: asPrecision(r.geo_precision), c: str(r.country_iso2) };203    const ai = str(r.ai_evidence);204    if (r.is_ai === true || ai === "confirmed" || ai === "likely") p.ai = 1;205    if (r.is_hyperscale === true) p.hs = 1;206    const y = num(r.y);207    if (y != null) p.y = y;208    const rs = str(r.record_scope);209    if (rs === "building" || rs === "campus") p.rs = rs;210    return p;211  });212}213214async function cloudRegionPoints(sql: Sql, q: MapQuery): Promise<MapPoint[]> {215  const c: Fragment[] = [sql`r.lat is not null and r.lng is not null`, bboxCond(sql, q.bbox, sql`r.lat`, sql`r.lng`)];216  if (q.country?.length) c.push(sql`r.country_iso2 = any(${q.country})`);217  if (q.operator) c.push(sql`(pr.slug = ${q.operator} or pr.id = ${q.operator})`);218  if (q.metro) c.push(sql`r.metro_id in (select id from metros where slug = ${q.metro} or id = ${q.metro})`);219  const rows = await sql<Row[]>`select r.id, r.slug, r.name, pr.name as pr_name, r.lat, r.lng, r.status, r.geo_precision, r.country_iso2, ${yearExpr(sql, sql`r.launched_on`)} as y from cloud_regions r join operators pr on pr.id = r.provider_id where ${andAll(sql, c)} limit 2000`;220  return rows.map((r) => {221    const st = str(r.status);222    const p: MapPoint = { id: String(r.id), slug: String(r.slug), n: String(r.name), o: str(r.pr_name), lat: num(r.lat) ?? 0, lng: num(r.lng) ?? 0, s: st === "announced" ? "announced" : st === "retired" ? "closed" : "operational", t: "cloud_region", p: asPrecision(r.geo_precision ?? "city"), c: str(r.country_iso2), k: "cloud_region" };223    const y = num(r.y);224    if (y != null) p.y = y;225    return p;226  });227}228229/** Live projects with coordinates (city-level geocodes → p = city). */230async function projectPoints(sql: Sql, q: MapQuery, onlyAi = false): Promise<MapPoint[]> {231  const c: Fragment[] = [projectLive(sql), sql`p.lat is not null and p.lng is not null`, bboxCond(sql, q.bbox, sql`p.lat`, sql`p.lng`)];232  if (q.country?.length) c.push(sql`p.country_iso2 = any(${q.country})`);233  if (q.operator) c.push(sql`p.operator_id in (select id from operators where slug = ${q.operator} or id = ${q.operator})`);234  if (q.metro) c.push(sql`p.metro_id in (select id from metros where slug = ${q.metro} or id = ${q.metro})`);235  if (q.project_status?.length) c.push(sql`p.status = any(${q.project_status})`);236  else if (q.layer === "pipeline") c.push(sql`p.status = any(${PIPELINE_PROJECT_STATUSES})`);237  if (q.expected_from != null) c.push(sql`${yearExpr(sql, sql`p.expected_opening`)} >= ${q.expected_from}`);238  if (q.expected_to != null) c.push(sql`${yearExpr(sql, sql`p.expected_opening`)} <= ${q.expected_to}`);239  if (q.min_mw != null) c.push(sql`p.planned_mw >= ${q.min_mw}`);240  if (q.max_mw != null) c.push(sql`p.planned_mw <= ${q.max_mw}`);241  if (onlyAi || q.ai === true) c.push(sql`(p.is_ai or p.ai_evidence in ('confirmed', 'likely'))`);242  if (q.ai_evidence?.length) c.push(sql`p.ai_evidence = any(${q.ai_evidence})`);243  if (q.year != null) c.push(sql`${yearExpr(sql, sql`coalesce(p.announced_on, p.expected_opening)`)} <= ${q.year}`);244  const rows = await sql<Row[]>`245    select p.id, p.slug, p.name, o.name as op_name, p.lat, p.lng, p.status, p.planned_mw, p.geo_precision, p.is_ai, p.ai_evidence, p.country_iso2, pf.facility_type, ${yearExpr(sql, sql`p.expected_opening`)} as y246    from projects p left join operators o on o.id = p.operator_id left join facilities pf on pf.id = p.facility_id247    where ${andAll(sql, c)} order by p.planned_mw desc nulls last, p.id limit ${MAX_POINTS}`;248  return rows.map((r) => {249    const p: MapPoint = { id: String(r.id), slug: String(r.slug), n: String(r.name), o: str(r.op_name), lat: num(r.lat) ?? 0, lng: num(r.lng) ?? 0, s: asStatus(r.status), t: r.facility_type ? asType(r.facility_type) : "unknown", mw: num(r.planned_mw), p: asPrecision(r.geo_precision), c: str(r.country_iso2), k: "project" };250    const ai = str(r.ai_evidence);251    if (r.is_ai === true || ai === "confirmed" || ai === "likely") p.ai = 1;252    const y = num(r.y);253    if (y != null) p.y = y;254    return p;255  });256}257258/** IXPs at their metro's reference point (ixps carry no coordinates); metro resolved by id, else by city name. */259function ixpMetroJoin(sql: Sql): Fragment {260  return sql`261    left join metros xm on xm.id = x.metro_id262    left join lateral (select m2.id, m2.slug, m2.name, m2.lat, m2.lng from metros m2 where x.metro_id is null and x.country_iso2 = m2.country_iso2 and x.city is not null and (lower(m2.name) = lower(x.city) or exists (select 1 from unnest(m2.aliases) a where lower(a) = lower(x.city))) limit 1) xm2 on true`;263}264265async function ixpPoints(sql: Sql, q: MapQuery): Promise<MapPoint[]> {266  const lat = sql`coalesce(xm.lat, xm2.lat)`, lng = sql`coalesce(xm.lng, xm2.lng)`;267  const c: Fragment[] = [sql`${lat} is not null`, bboxCond(sql, q.bbox, lat, lng)];268  if (q.country?.length) c.push(sql`x.country_iso2 = any(${q.country})`);269  if (q.metro) c.push(sql`coalesce(xm.id, xm2.id) in (select id from metros where slug = ${q.metro} or id = ${q.metro})`);270  const rows = await sql<Row[]>`select x.id, x.slug, x.name, x.country_iso2, x.network_count, ${lat} as lat, ${lng} as lng from ixps x ${ixpMetroJoin(sql)} where ${andAll(sql, c)} order by x.network_count desc nulls last limit 2000`;271  return rows.map((r) => ({ id: String(r.id), slug: String(r.slug), n: String(r.name), o: null, lat: num(r.lat) ?? 0, lng: num(r.lng) ?? 0, s: "operational", t: "internet_exchange", mw: null, p: "metro", c: str(r.country_iso2), k: "ixp" }));272}273274/** Power / grid events by metro, as clusters at the metro reference point (no invented plant locations). */275async function powerEventClusters(sql: Sql, q: MapQuery): Promise<MapCluster[]> {276  const types = [...new Set([...POWER_EVENT_TYPES, ...GRID_EVENT_TYPES])];277  const c: Fragment[] = [sql`e.event_type = any(${types})`, sql`e.review_status <> 'rejected'`, bboxCond(sql, q.bbox, sql`m.lat`, sql`m.lng`)];278  if (q.country?.length) c.push(sql`m.country_iso2 = any(${q.country})`);279  if (q.metro) c.push(sql`(m.slug = ${q.metro} or m.id = ${q.metro})`);280  const rows = await sql<Row[]>`select m.id, m.name, m.lat, m.lng, count(*)::int as n from events e join metros m on m.id = e.metro_id where ${andAll(sql, c)} group by m.id, m.name, m.lat, m.lng order by n desc limit 500`;281  return rows.map((r) => ({ key: `power:${String(r.id)}`, lat: num(r.lat) ?? 0, lng: num(r.lng) ?? 0, count: int(r.n), mw: null, statuses: {}, label: `${String(r.name)} — ${int(r.n)} power / grid event${int(r.n) > 1 ? "s" : ""}` }));282}283284/** Share of filter-matching facilities that carry an opening year (time machine honesty figure). */285async function yearCoverage(sql: Sql, q: MapQuery): Promise<number> {286  const rows = await sql<Row[]>`select count(*)::int as n, count(*) filter (where f.opened_on ~ '^\\d{4}')::int as dated from facilities f ${facilityJoins(sql)} where ${andAll(sql, baseConds(sql, q))}`;287  const n = int(rows[0]?.n);288  return n ? Math.round((int(rows[0]?.dated) / n) * 1000) / 1000 : 0;289}290291async function densityCells(sql: Sql, q: MapQuery, zoom: number): Promise<{ view: DensityView; cells: DensityCell[]; max: number; total: number }> {292  const view = q.density ?? "facilities";293  const size = cellSize(zoom);294  const cell = (latCol: Fragment, lngCol: Fragment) => sql`floor((${lngCol} + 180) / ${size})::int as i, floor((${latCol} + 90) / ${size})::int as j`;295  let rows: Row[];296  if (view === "cloud") {297    const c: Fragment[] = [sql`r.lat is not null and r.lng is not null and r.status <> 'retired'`, bboxCond(sql, q.bbox, sql`r.lat`, sql`r.lng`)];298    if (q.country?.length) c.push(sql`r.country_iso2 = any(${q.country})`);299    if (q.operator) c.push(sql`r.provider_id in (select id from operators where slug = ${q.operator} or id = ${q.operator})`);300    rows = await sql<Row[]>`select ${cell(sql`r.lat`, sql`r.lng`)}, count(*)::int as n, count(*)::float as w from cloud_regions r where ${andAll(sql, c)} group by 1, 2`;301  } else if (view === "ixps") {302    const lat = sql`coalesce(xm.lat, xm2.lat)`, lng = sql`coalesce(xm.lng, xm2.lng)`;303    const c: Fragment[] = [sql`${lat} is not null`, bboxCond(sql, q.bbox, lat, lng)];304    if (q.country?.length) c.push(sql`x.country_iso2 = any(${q.country})`);305    rows = await sql<Row[]>`select ${cell(lat, lng)}, count(*)::int as n, count(*)::float as w from ixps x ${ixpMetroJoin(sql)} where ${andAll(sql, c)} group by 1, 2`;306  } else {307    const weight: Fragment =308      view === "known_mw" ? sql`coalesce(sum(${knownMwAgg(sql)}) filter (where f.status in ('operational', 'partially_operational', 'expansion')), 0)::float`309      : view === "pipeline_mw" ? sql`coalesce(sum(${pipelineMwAgg(sql)}) filter (where f.status = any(${PIPELINE_SET})), 0)::float`310      : view === "ai" ? sql`count(*) filter (where ${countedAgg(sql)} and ${AI_COND(sql)})::float`311      : view === "operators" ? sql`count(distinct f.operator_id)::float`312      : sql`count(*) filter (where ${countedAgg(sql)})::float`;313    rows = await sql<Row[]>`select ${cell(sql`f.lat`, sql`f.lng`)}, count(*)::int as n, ${weight} as w314      from ${facilityView(sql)} f ${facilityJoins(sql)} where ${conds(sql, q)} group by 1, 2`;315  }316  const cells: DensityCell[] = [];317  let max = 0, total = 0;318  for (const r of rows) {319    const w = Math.round((num(r.w) ?? 0) * 100) / 100;320    const n = int(r.n);321    if (w <= 0 && n <= 0) continue;322    total += n;323    if (w > max) max = w;324    cells.push({ lat: Math.round((int(r.j) * size - 90 + size / 2) * 1e4) / 1e4, lng: Math.round((int(r.i) * size - 180 + size / 2) * 1e4) / 1e4, w, n });325  }326  cells.sort((a, b) => b.w - a.w);327  return { view, cells, max, total };328}329330/** Overlay points for the requested layer (non-facility entities). */331async function overlayFor(sql: Sql, q: MapQuery): Promise<{ overlay: MapPoint[]; clusters: MapCluster[] }> {332  const layer = q.layer ?? "facilities";333  const tasks: Array<Promise<MapPoint[]>> = [];334  let clusters: Promise<MapCluster[]> = Promise.resolve([]);335  if (layer === "projects" || layer === "pipeline") tasks.push(projectPoints(sql, q));336  if (layer === "ai") tasks.push(projectPoints(sql, q, true));337  if (layer === "cloud" || layer === "connectivity" || q.cloud_regions) tasks.push(cloudRegionPoints(sql, q));338  if (layer === "ixps" || layer === "connectivity") tasks.push(ixpPoints(sql, q));339  if (layer === "power") clusters = powerEventClusters(sql, q);340  const [parts, cl] = await Promise.all([Promise.all(tasks), clusters]);341  return { overlay: parts.flat(), clusters: cl };342}343344export async function mapData(q: MapQuery): Promise<MapResponse> {345  const sql = pg();346  const zoom = Math.max(0, Math.min(22, Math.round(q.zoom)));347  const layer: MapLayer = q.layer ?? "facilities";348  const withOverlay = (res: MapResponse, o: { overlay: MapPoint[]; clusters: MapCluster[] }): MapResponse => {349    if (o.overlay.length) res.overlay = o.overlay;350    // API 1.x compatibility: the legacy `cloud_regions=1` flag (no `layer`) returned cloud regions inside `points`351    if (o.overlay.length && q.cloud_regions && !q.layer) res.points = [...(res.points ?? []), ...o.overlay.filter((p) => p.k === "cloud_region")];352    if (o.clusters.length) res.clusters = [...(res.clusters ?? []), ...o.clusters];353    return res;354  };355  const year = q.year ?? null;356  const cov = year != null ? yearCoverage(sql, q) : Promise.resolve(null);357  const overlayP = overlayFor(sql, q);358359  if (q.density) {360    const [d, o, yc] = await Promise.all([densityCells(sql, q, zoom), overlayP, cov]);361    const res: MapResponse = { zoom, mode: "density", layer, density: { view: d.view, cells: d.cells, max: d.max }, total: d.total, year, yearCoverage: yc };362    return withOverlay(res, o);363  }364365  if (!facilitiesDrawn(q)) {366    const [o, yc] = await Promise.all([overlayP, cov]);367    const res: MapResponse = { zoom, mode: "points", layer, points: [], total: o.overlay.length, year, yearCoverage: yc };368    return withOverlay(res, o);369  }370371  const mode = mapMode(zoom);372  if (mode === "country" || mode === "grid") {373    const [clusters, o, yc] = await Promise.all([mode === "country" ? countryClusters(sql, q) : gridClusters(sql, q, zoom), overlayP, cov]);374    const res: MapResponse = { zoom, mode: "clusters", layer, clusters, total: clusters.reduce((a, c) => a + c.count, 0), year, yearCoverage: yc };375    return withOverlay(res, o);376  }377  const [pts, o, yc] = await Promise.all([points(sql, q), overlayP, cov]);378  if (pts === null) {379    const clusters = await gridClusters(sql, q, 8);380    const res: MapResponse = { zoom, mode: "clusters", layer, clusters, total: clusters.reduce((a, c) => a + c.count, 0), degraded: true, year, yearCoverage: yc };381    return withOverlay(res, o);382  }383  const res: MapResponse = { zoom, mode: "points", layer, points: pts, total: pts.length, year, yearCoverage: yc };384  return withOverlay(res, o);385}386