/** * /map data: zoom-tiered clusters / points for facilities, thematic layers (capacity, projects, AI, cloud, IXPs, * connectivity, power, pipeline), density grids and a "time machine" year filter. Only entities with coordinates are * drawn; IXPs without a published location fall back to their metro reference point (p = "metro"). Layers without a * connector (landing stations, substations, power plants) are never drawn — power events are exposed as metro-level * clusters, not as invented plant locations. */ import type { DensityCell, DensityView, FacilityStatus, MapCluster, MapLayer, MapPoint, MapResponse } from "@dci/core"; import { 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"; import { facilityConds, type FacilityQuery } from "./facilities.js"; import { int, num, str, type Row } from "../lib/rows.js"; import { asPrecision, asStatus, asType } from "../lib/dto.js"; export interface Bbox { w: number; s: number; e: number; n: number } export interface MapQuery { zoom: number; bbox?: Bbox; status?: string[]; type?: string[]; operator?: string; metro?: string; country?: string[]; q?: string; min_mw?: number; max_mw?: number; ai?: boolean; hyperscale?: boolean; has_mw?: boolean; confidence?: string[]; opened_from?: number; opened_to?: number; cloud_regions?: boolean; layer?: MapLayer; density?: DensityView; year?: number; project_status?: string[]; expected_from?: number; expected_to?: number; location_precision?: string[]; ai_evidence?: string[]; } export const MAX_POINTS = 5000; export 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."; /** Map mode from zoom: <5 countries, 5–8 grid, ≥9 points. Exported for tests. */ export function mapMode(zoom: number): "country" | "grid" | "points" { if (zoom < 5) return "country"; if (zoom < 9) return "grid"; return "points"; } export function cellSize(zoom: number): number { return 360 / Math.pow(2, Math.max(0, Math.min(zoom, 12)) + 1); } function bboxCond(sql: Sql, b: Bbox | undefined, latCol: Fragment, lngCol: Fragment): Fragment { if (!b) return sql`true`; const lng = b.w <= b.e ? sql`${lngCol} between ${b.w} and ${b.e}` : sql`(${lngCol} >= ${b.w} or ${lngCol} <= ${b.e})`; return sql`(${latCol} between ${b.s} and ${b.n} and ${lng})`; } const AI_COND = (sql: Sql) => sql`(f.ai_evidence in ('confirmed', 'likely') or f.is_ai)`; const PIPELINE_PROJECT_STATUSES = ["rumored", "proposed", "announced", "permitting", "approved", "under_construction", "delayed"]; /** Layer-specific facility conditions (on top of the generic filters). */ function layerConds(sql: Sql, q: MapQuery): Fragment[] { const out: Fragment[] = []; switch (q.layer) { case "capacity": out.push(sql`${mwExpr(sql)} is not null`); break; case "ai": out.push(AI_COND(sql)); break; case "power": out.push(sql`(f.utility_capacity_mw is not null or f.grid_connection_mw is not null)`); break; case "pipeline": out.push(sql`f.status = any(${PIPELINE_SET})`); break; case "connectivity": out.push(sql`(f.facility_type = 'carrier_hotel' or coalesce(f.carriers_count, 0) >= 20)`); break; default: break; } if (q.location_precision?.length) out.push(sql`f.geo_precision = any(${q.location_precision})`); if (q.ai_evidence?.length) out.push(sql`f.ai_evidence = any(${q.ai_evidence})`); return out; } /** Time-machine condition: opened_on year ≤ year; pipeline rows only when the caller asked for pipeline statuses. */ function yearCond(sql: Sql, q: MapQuery): Fragment { if (q.year == null) return sql`true`; const opened = sql`${yearExpr(sql, sql`f.opened_on`)} <= ${q.year}`; const pipelineAsked = (q.status ?? []).some((s) => (PIPELINE_SET as string[]).includes(s)); if (!pipelineAsked) return opened; 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}))`; } function baseConds(sql: Sql, q: MapQuery): Fragment[] { 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 }; 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`)]; } function conds(sql: Sql, q: MapQuery): Fragment { return andAll(sql, [...baseConds(sql, q), yearCond(sql, q)]); } /** Layers whose facility points are hidden (overlay-only layers). */ function facilitiesDrawn(q: MapQuery): boolean { return q.layer !== "cloud" && q.layer !== "ixps" && q.layer !== "projects"; } function addStatus(target: Partial>, status: unknown, n: number): void { const s = asStatus(status); target[s] = (target[s] ?? 0) + n; } /** MW column drawn for the layer: power → utility/grid supply, else the best known figure. */ function pointMw(sql: Sql, q: MapQuery): Fragment { return q.layer === "power" ? sql`coalesce(f.utility_capacity_mw, f.grid_connection_mw)` : mwExpr(sql); } async function countryClusters(sql: Sql, q: MapQuery): Promise { const mw = pointMw(sql, q); const rows = await sql` 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 cname from facilities f ${facilityJoins(sql)} where ${conds(sql, q)} group by f.country_iso2, f.status, c.lat, c.lng, c.name`; const by = new Map(); for (const r of rows) { const key = str(r.country_iso2) ?? "unknown"; let c = by.get(key); 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); } const n = int(r.n); c.count += n; c._slat += num(r.slat) ?? 0; c._slng += num(r.slng) ?? 0; const m = num(r.mw); if (m != null) { c.mw = (c.mw ?? 0) + m; c._hasMw = true; } addStatus(c.statuses, r.status, n); const clat = num(r.clat), clng = num(r.clng); if (clat != null && clng != null) { c.lat = clat; c.lng = clng; } } // Facilities without a resolved country must not collapse into one meaningless blob at their mean coordinate: // re-cluster them on a coarse grid (zoom-4 cells) instead. const unknown = by.get("unknown"); if (unknown) { by.delete("unknown"); const size = cellSize(4); const cells = await sql` 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 alng from facilities f ${facilityJoins(sql)} where ${conds(sql, q)} and f.country_iso2 is null group by 1, 2, 3`; for (const r of cells) { const key = `cell:${int(r.i)}:${int(r.j)}`; let c = by.get(key); 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); } const n = int(r.n); c.count += n; const m = num(r.mw); if (m != null) { c.mw = (c.mw ?? 0) + m; c._hasMw = true; } addStatus(c.statuses, r.status, n); } } return [...by.values()].map((c) => { const { _slat, _slng, _hasMw, ...rest } = c; if (!rest.lat && !rest.lng && rest.count) { rest.lat = _slat / rest.count; rest.lng = _slng / rest.count; } rest.lat = Math.round(rest.lat * 1e4) / 1e4; rest.lng = Math.round(rest.lng * 1e4) / 1e4; if (rest.mw != null) rest.mw = Math.round(rest.mw * 100) / 100; return rest; }).sort((a, b) => b.count - a.count); } async function gridClusters(sql: Sql, q: MapQuery, zoom: number): Promise { const size = cellSize(zoom); const mw = pointMw(sql, q); const rows = await sql` 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 from facilities f ${facilityJoins(sql)} where ${conds(sql, q)} group by 1, 2, 3`; const by = new Map(); for (const r of rows) { const i = int(r.i), j = int(r.j); const key = `${zoom}:${i}:${j}`; let c = by.get(key); 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); } const n = int(r.n); c.count += n; const m = num(r.mw); if (m != null) c.mw = Math.round(((c.mw ?? 0) + m) * 100) / 100; addStatus(c.statuses, r.status, n); } return [...by.values()].sort((a, b) => b.count - a.count); } async function points(sql: Sql, q: MapQuery): Promise { const mw = pointMw(sql, q); const rows = await sql` 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 y from facilities f ${facilityJoins(sql)} where ${conds(sql, q)} order by ${mw} desc nulls last, f.id limit ${MAX_POINTS + 1}`; if (rows.length > MAX_POINTS) return null; return rows.map((r) => { 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) }; const ai = str(r.ai_evidence); if (r.is_ai === true || ai === "confirmed" || ai === "likely") p.ai = 1; if (r.is_hyperscale === true) p.hs = 1; const y = num(r.y); if (y != null) p.y = y; const rs = str(r.record_scope); if (rs === "building" || rs === "campus") p.rs = rs; return p; }); } async function cloudRegionPoints(sql: Sql, q: MapQuery): Promise { 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`)]; if (q.country?.length) c.push(sql`r.country_iso2 = any(${q.country})`); if (q.operator) c.push(sql`(pr.slug = ${q.operator} or pr.id = ${q.operator})`); if (q.metro) c.push(sql`r.metro_id in (select id from metros where slug = ${q.metro} or id = ${q.metro})`); const rows = await sql`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`; return rows.map((r) => { const st = str(r.status); 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" }; const y = num(r.y); if (y != null) p.y = y; return p; }); } /** Live projects with coordinates (city-level geocodes → p = city). */ async function projectPoints(sql: Sql, q: MapQuery, onlyAi = false): Promise { 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`)]; if (q.country?.length) c.push(sql`p.country_iso2 = any(${q.country})`); if (q.operator) c.push(sql`p.operator_id in (select id from operators where slug = ${q.operator} or id = ${q.operator})`); if (q.metro) c.push(sql`p.metro_id in (select id from metros where slug = ${q.metro} or id = ${q.metro})`); if (q.project_status?.length) c.push(sql`p.status = any(${q.project_status})`); else if (q.layer === "pipeline") c.push(sql`p.status = any(${PIPELINE_PROJECT_STATUSES})`); if (q.expected_from != null) c.push(sql`${yearExpr(sql, sql`p.expected_opening`)} >= ${q.expected_from}`); if (q.expected_to != null) c.push(sql`${yearExpr(sql, sql`p.expected_opening`)} <= ${q.expected_to}`); if (q.min_mw != null) c.push(sql`p.planned_mw >= ${q.min_mw}`); if (q.max_mw != null) c.push(sql`p.planned_mw <= ${q.max_mw}`); if (onlyAi || q.ai === true) c.push(sql`(p.is_ai or p.ai_evidence in ('confirmed', 'likely'))`); if (q.ai_evidence?.length) c.push(sql`p.ai_evidence = any(${q.ai_evidence})`); if (q.year != null) c.push(sql`${yearExpr(sql, sql`coalesce(p.announced_on, p.expected_opening)`)} <= ${q.year}`); const rows = await sql` 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 y from projects p left join operators o on o.id = p.operator_id left join facilities pf on pf.id = p.facility_id where ${andAll(sql, c)} order by p.planned_mw desc nulls last, p.id limit ${MAX_POINTS}`; return rows.map((r) => { 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" }; const ai = str(r.ai_evidence); if (r.is_ai === true || ai === "confirmed" || ai === "likely") p.ai = 1; const y = num(r.y); if (y != null) p.y = y; return p; }); } /** IXPs at their metro's reference point (ixps carry no coordinates); metro resolved by id, else by city name. */ function ixpMetroJoin(sql: Sql): Fragment { return sql` left join metros xm on xm.id = x.metro_id 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`; } async function ixpPoints(sql: Sql, q: MapQuery): Promise { const lat = sql`coalesce(xm.lat, xm2.lat)`, lng = sql`coalesce(xm.lng, xm2.lng)`; const c: Fragment[] = [sql`${lat} is not null`, bboxCond(sql, q.bbox, lat, lng)]; if (q.country?.length) c.push(sql`x.country_iso2 = any(${q.country})`); if (q.metro) c.push(sql`coalesce(xm.id, xm2.id) in (select id from metros where slug = ${q.metro} or id = ${q.metro})`); const rows = await sql`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`; 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" })); } /** Power / grid events by metro, as clusters at the metro reference point (no invented plant locations). */ async function powerEventClusters(sql: Sql, q: MapQuery): Promise { const types = [...new Set([...POWER_EVENT_TYPES, ...GRID_EVENT_TYPES])]; const c: Fragment[] = [sql`e.event_type = any(${types})`, sql`e.review_status <> 'rejected'`, bboxCond(sql, q.bbox, sql`m.lat`, sql`m.lng`)]; if (q.country?.length) c.push(sql`m.country_iso2 = any(${q.country})`); if (q.metro) c.push(sql`(m.slug = ${q.metro} or m.id = ${q.metro})`); const rows = await sql`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`; 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" : ""}` })); } /** Share of filter-matching facilities that carry an opening year (time machine honesty figure). */ async function yearCoverage(sql: Sql, q: MapQuery): Promise { const rows = await sql`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))}`; const n = int(rows[0]?.n); return n ? Math.round((int(rows[0]?.dated) / n) * 1000) / 1000 : 0; } async function densityCells(sql: Sql, q: MapQuery, zoom: number): Promise<{ view: DensityView; cells: DensityCell[]; max: number; total: number }> { const view = q.density ?? "facilities"; const size = cellSize(zoom); const cell = (latCol: Fragment, lngCol: Fragment) => sql`floor((${lngCol} + 180) / ${size})::int as i, floor((${latCol} + 90) / ${size})::int as j`; let rows: Row[]; if (view === "cloud") { 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`)]; if (q.country?.length) c.push(sql`r.country_iso2 = any(${q.country})`); if (q.operator) c.push(sql`r.provider_id in (select id from operators where slug = ${q.operator} or id = ${q.operator})`); rows = await sql`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`; } else if (view === "ixps") { const lat = sql`coalesce(xm.lat, xm2.lat)`, lng = sql`coalesce(xm.lng, xm2.lng)`; const c: Fragment[] = [sql`${lat} is not null`, bboxCond(sql, q.bbox, lat, lng)]; if (q.country?.length) c.push(sql`x.country_iso2 = any(${q.country})`); rows = await sql`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`; } else { const weight: Fragment = view === "known_mw" ? sql`coalesce(sum(${knownMwAgg(sql)}) filter (where f.status in ('operational', 'partially_operational', 'expansion')), 0)::float` : view === "pipeline_mw" ? sql`coalesce(sum(${pipelineMwAgg(sql)}) filter (where f.status = any(${PIPELINE_SET})), 0)::float` : view === "ai" ? sql`count(*) filter (where ${countedAgg(sql)} and ${AI_COND(sql)})::float` : view === "operators" ? sql`count(distinct f.operator_id)::float` : sql`count(*) filter (where ${countedAgg(sql)})::float`; rows = await sql`select ${cell(sql`f.lat`, sql`f.lng`)}, count(*)::int as n, ${weight} as w from ${facilityView(sql)} f ${facilityJoins(sql)} where ${conds(sql, q)} group by 1, 2`; } const cells: DensityCell[] = []; let max = 0, total = 0; for (const r of rows) { const w = Math.round((num(r.w) ?? 0) * 100) / 100; const n = int(r.n); if (w <= 0 && n <= 0) continue; total += n; if (w > max) max = w; 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 }); } cells.sort((a, b) => b.w - a.w); return { view, cells, max, total }; } /** Overlay points for the requested layer (non-facility entities). */ async function overlayFor(sql: Sql, q: MapQuery): Promise<{ overlay: MapPoint[]; clusters: MapCluster[] }> { const layer = q.layer ?? "facilities"; const tasks: Array> = []; let clusters: Promise = Promise.resolve([]); if (layer === "projects" || layer === "pipeline") tasks.push(projectPoints(sql, q)); if (layer === "ai") tasks.push(projectPoints(sql, q, true)); if (layer === "cloud" || layer === "connectivity" || q.cloud_regions) tasks.push(cloudRegionPoints(sql, q)); if (layer === "ixps" || layer === "connectivity") tasks.push(ixpPoints(sql, q)); if (layer === "power") clusters = powerEventClusters(sql, q); const [parts, cl] = await Promise.all([Promise.all(tasks), clusters]); return { overlay: parts.flat(), clusters: cl }; } export async function mapData(q: MapQuery): Promise { const sql = pg(); const zoom = Math.max(0, Math.min(22, Math.round(q.zoom))); const layer: MapLayer = q.layer ?? "facilities"; const withOverlay = (res: MapResponse, o: { overlay: MapPoint[]; clusters: MapCluster[] }): MapResponse => { if (o.overlay.length) res.overlay = o.overlay; // API 1.x compatibility: the legacy `cloud_regions=1` flag (no `layer`) returned cloud regions inside `points` if (o.overlay.length && q.cloud_regions && !q.layer) res.points = [...(res.points ?? []), ...o.overlay.filter((p) => p.k === "cloud_region")]; if (o.clusters.length) res.clusters = [...(res.clusters ?? []), ...o.clusters]; return res; }; const year = q.year ?? null; const cov = year != null ? yearCoverage(sql, q) : Promise.resolve(null); const overlayP = overlayFor(sql, q); if (q.density) { const [d, o, yc] = await Promise.all([densityCells(sql, q, zoom), overlayP, cov]); const res: MapResponse = { zoom, mode: "density", layer, density: { view: d.view, cells: d.cells, max: d.max }, total: d.total, year, yearCoverage: yc }; return withOverlay(res, o); } if (!facilitiesDrawn(q)) { const [o, yc] = await Promise.all([overlayP, cov]); const res: MapResponse = { zoom, mode: "points", layer, points: [], total: o.overlay.length, year, yearCoverage: yc }; return withOverlay(res, o); } const mode = mapMode(zoom); if (mode === "country" || mode === "grid") { const [clusters, o, yc] = await Promise.all([mode === "country" ? countryClusters(sql, q) : gridClusters(sql, q, zoom), overlayP, cov]); const res: MapResponse = { zoom, mode: "clusters", layer, clusters, total: clusters.reduce((a, c) => a + c.count, 0), year, yearCoverage: yc }; return withOverlay(res, o); } const [pts, o, yc] = await Promise.all([points(sql, q), overlayP, cov]); if (pts === null) { const clusters = await gridClusters(sql, q, 8); const res: MapResponse = { zoom, mode: "clusters", layer, clusters, total: clusters.reduce((a, c) => a + c.count, 0), degraded: true, year, yearCoverage: yc }; return withOverlay(res, o); } const res: MapResponse = { zoom, mode: "points", layer, points: pts, total: pts.length, year, yearCoverage: yc }; return withOverlay(res, o); }