/** Operators2 — Asia-Pacific facility parsers. */ import type { ExtractedRecord } from "@dci/connectors"; import { countryFromText } from "@dci/core"; import { Rec, capacityMw, certifications, countryFromCity, countryOf, describe, facilityParser, first, ha, itMw, key, mw, pue, seg, sqm, statusOf, tier, titleCase } from "./shared.js"; /** AirTrunk — /location/-/ ; "Key features" list: capacity, size (ha, m2), design PUE. */ facilityParser("airtrunk_facility_v1", (p) => { const h1 = p.h1 ?? ""; const m = h1.match(/^([A-Z]{3}\d)\s+(.+)$/); if (!m || /\/(zh-hans|zh|ja|ko)\//.test(p.url)) return []; const r = new Rec(); const city = m[2]!.trim(); r.set("name", `AirTrunk ${h1}`, "selector:h1").set("code", m[1], "selector:h1").set("city", city, "selector:h1"); const c = countryOf(city, p.text.slice(0, 1500)); if (c) r.set("countryIso2", c.iso, c.method); r.set("totalPowerMw", mw(first(p.text, /(?:total\s+)?capacity of (\d[\d.,]*\+?\s*MW)/i)) ?? capacityMw(p.text), "regex:capacity_of_mw").set("itCapacityMw", itMw(p.text), "regex:it_mw_v1"); r.set("siteAreaHa", ha(first(p.text, /Set on (\d[\d.,]*\s*ha)/i)), "regex:set_on_ha").set("buildingSqm", sqm(first(p.text, /([\d,.]+\s*m2) of built-up area/i)), "regex:built_up_area").set("pue", pue(p.text), "regex:pue_v1"); r.set("facilityType", "hyperscale", "default:operator").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p) ?? first(p.text, /Location\s*\n\s*([^\n]{20,200})/), "meta:description"); const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("airtrunk", m[1]), p.url)]; }); /** NEXTDC — /data-centres/-data-centres/- ; IT capacity ("12MW", "20MW+", "10+MW"), technical space, rack capacity, Tier III. */ facilityParser("nextdc_facility_v1", (p) => { const slug = seg(p.url).pop() ?? ""; const m = slug.match(/^([a-z]{1,2}\d{1,2})-([a-z-]+)$/); if (!m) return []; const code = m[1]!.toUpperCase(); const r = new Rec(); r.set("name", `NEXTDC ${code} ${titleCase(m[2]!)}`, "url:slug").set("code", code, "url:slug").set("city", titleCase(m[2]!), "url:slug").set("countryIso2", "AU", "default:operator"); r.set("itCapacityMw", mw(first(p.text, /(\d[\d.,]*\+?\s*MW\+?)\s*\n?\s*IT Capacity/i)) ?? itMw(p.text), "regex:it_capacity_stat").set("buildingSqm", sqm(first(p.text, /([\d,.]+\s*m\s?2)\s*\n?\s*Technical Space/i)), "regex:technical_space_stat"); const racks = first(p.text, /([\d,]+)\s*\n?\s*Rack Capacity/i); if (racks) r.set("rackCount", Number(racks.replace(/,/g, "")), "regex:rack_capacity_stat"); const addr = p.text.match(/\n\s*(\d{1,4}[-–]?\d{0,4}\s+[A-Z][^\n,]{3,60}),?\s*\n?\s*([A-Z][a-zA-Z ]+?)\s+(NSW|VIC|QLD|SA|WA|ACT|NT|TAS)\s+(\d{4})\b/); if (addr) r.set("address", addr[1], "regex:au_address").set("regionName", addr[3], "regex:au_address").set("postalCode", addr[4], "regex:au_address"); r.set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("nextdc", code), p.url)]; }); /** Sify — /data-center/NN-data-center/ ; IT power, Rated 3, certifications. HQ JSON-LD is ignored on purpose. */ facilityParser("sify_facility_v1", (p) => { const slug = seg(p.url).pop() ?? ""; const m = slug.match(/^([a-z]+?)(\d{2})?-data-center$/); if (!m) return []; const city = titleCase(m[1]!); const code = `${city} ${m[2] ?? "01"}`; const r = new Rec(); r.set("name", `Sify ${code}`, "url:slug").set("code", code.replace(/\s/g, ""), "url:slug").set("city", city, "url:slug").set("countryIso2", "IN", "default:operator"); const campus = first(p.text, /\b(?:in|at)\s+([A-Z][a-zA-Z]+)\s+(?:has|campus)/); if (campus && campus !== city) r.set("campusName", `Sify ${campus}`, "regex:campus"); const own = p.text.slice(0, 4000).replace(/\b\d+\s+pan-India[^.]*\./gi, "").replace(/\b\d+\s+Data Centers-\s*\d+\s*MW[^.\n]*/gi, ""); r.set("itCapacityMw", itMw(own), "regex:it_mw_v1").set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); if (/AI[- ]ready|hyperscale/i.test(`${p.title ?? ""} ${p.desc ?? ""}`)) r.set("isAi", /AI[- ]ready/i.test(`${p.title ?? ""} ${p.desc ?? ""}`), "regex:ai_ready"); return [r.done(key("sify", code), p.url, 0.8)]; }); /** Digital Edge — /resources/data-center// ; " Data Center", campus MW at full capacity. */ facilityParser("digitaledge_facility_v1", (p) => { const parts = seg(p.url); if (parts.includes("cn") || parts.includes("jp")) return []; const code = (p.h1 ?? "").trim(); if (!/^[A-Z]{3,5}\d$/.test(code)) return []; const r = new Rec(); const city = first(p.text, new RegExp(`${code}\\s*\\n?\\s*([A-Z][a-zA-Z ]+?)\\s+Data Center`)) ?? first(p.title ?? "", /\b[A-Z]{3,5}\d\s+([A-Z][a-zA-Z ]+?)\s+Data Center/); r.set("name", `Digital Edge ${code}${city ? ` ${city}` : ""}`, "selector:h1").set("code", code, "selector:h1").set("city", city, "regex:code_city"); const c = countryOf(p.desc, city, p.text.slice(0, 2000)); if (c) r.set("countryIso2", c.iso, c.method); const full = first(p.text, /(\d[\d.,]*\+?\s*MW\+?) campus at full capacity/i); if (full) r.set("plannedPowerMw", mw(full), "regex:full_capacity_campus"); r.set("itCapacityMw", itMw(p.text.slice(0, 3000)), "regex:it_mw_v1").set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); const st = statusOf(p.text.slice(0, 3000)) ?? (/expected to be complete|will be constructed/i.test(p.text.slice(0, 3000)) ? "under_construction" : null); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("digitaledge", code), p.url)]; }); /** CDC Data Centres — /locations// ; campus blocks "Hume Campus One … Current capacity (operating) 21 MW". */ facilityParser("cdc_campus_v1", (p) => { const city = p.h1 ?? titleCase(seg(p.url).pop() ?? ""); const out: ExtractedRecord[] = []; const iso = countryFromCity(city) ?? "AU"; const heads = [...p.text.matchAll(/\n\s*((?:[A-Z][A-Za-z]+ ){1,3}Campus(?: (?:One|Two|Three|Four|\d))?)\s*\n/g)].filter((h) => !/^(Data Cent|Our|The|Sydney|Melbourne|Canberra|Auckland|Perth) /.test(h[1]!)); for (let i = 0; i < heads.length; i++) { const h = heads[i]!; const start = (h.index ?? 0) + h[0].length; const end = Math.min(heads[i + 1]?.index ?? p.text.length, start + 1200); const block = p.text.slice(start, end).split(/\n\s*(?:Get in touch|Book a tour|Learn more)\s*\n/)[0] ?? ""; const campus = h[1]!.trim(); if (out.some((o) => o.key === key("cdc", city, campus))) continue; const r = new Rec(); r.set("name", `CDC ${campus}`, "regex:campus_block").set("campusName", `CDC ${campus}`, "regex:campus_block").set("city", city, "selector:h1").set("countryIso2", iso, "lookup:city-country"); const operating = first(block, /Current capacity \(operating\)\s*\n?\s*(\d[\d.,]*\+?\s*MW\+?)/i) ?? first(block, /(?:over|with) (\d[\d.,]*\+?\s*MW\+?) of capacity/i); const planned = first(block, /(\d[\d.,]*\+?\s*MW\+?) of planned capacity/i) ?? (/under construction|will be home|early stages/i.test(block) ? first(block, /Total capacity\s*\n?\s*(\d[\d.,]*\+?\s*MW\+?)/i) : null); r.set("totalPowerMw", mw(operating), "regex:campus_block").set("plannedPowerMw", mw(planned), "regex:campus_block"); if (!operating && planned) r.set("status", /under construction|early stages of construction/i.test(block) ? "under_construction" : "announced", "regex:campus_block"); else if (/under construction/i.test(block) && !/operational/i.test(block)) r.set("status", "under_construction", "regex:campus_block"); if (!operating && !planned) { const total = first(block, /Total capacity\s*\n?\s*(\d[\d.,]*\+?\s*MW\+?)/i); r.set("totalPowerMw", mw(total), "regex:campus_block"); } const codes = [...new Set([...block.matchAll(/\b([A-Z]{2}\d)\b/g)].map((x) => x[1]!))]; if (codes.length) r.set("aliases", codes, "regex:campus_block"); const blurb = block.split("\n").map((l) => l.trim()).find((l) => l.length > 30); r.set("facilityType", "hyperscale", "default:operator").set("description", blurb ?? describe(p), "regex:campus_block").set("certifications", certifications(p.text), "regex:certifications_v1"); if (r.has("totalPowerMw") || r.has("plannedPowerMw") || codes.length) out.push(r.done(key("cdc", city, campus), p.url, 0.8)); } return out; }); /** Princeton Digital Group — /locations// ; " - " sections with Colocation Area and Capacity. */ facilityParser("pdg_facility_v1", (p) => { const parts = seg(p.url); if (parts[0] !== "locations" || parts.length < 2) return []; const out: ExtractedRecord[] = []; const countryFromUrl = countryFromText(parts[1]!.replace(/-/g, " ")); const heads = [...p.text.matchAll(/\n\s*([A-Z][a-z]+(?: [A-Z][a-z]+)*)\s*[-–]\s*([A-Z]{2,3}\d{1,2})\s*\n/g)]; for (let i = 0; i < heads.length; i++) { const h = heads[i]!; const start = h.index ?? 0; const next = heads.slice(i + 1).find((x) => x[2] !== h[2]); const block = p.text.slice(start, next?.index ?? p.text.length); const city = h[1]!.trim(), code = h[2]!; if (out.some((o) => o.key === key("pdg", code))) continue; const r = new Rec(); r.set("name", `PDG ${code} ${city}`, "regex:city_code_heading").set("code", code, "regex:city_code_heading").set("city", city, "regex:city_code_heading"); const c = countryFromCity(city) ?? countryFromUrl; r.set("countryIso2", c, countryFromCity(city) ? "lookup:city-country" : "url:segment>country"); r.set("totalPowerMw", mw(first(block, /Capacity\s*\n?\s*(\d[\d.,]*\+?\s*MW\+?)/i)) ?? capacityMw(block), "regex:capacity_row").set("buildingSqm", sqm(first(block, /Colocation Area\s*\n?\s*([\d,.]+\s*m²)/i)), "regex:colocation_area_row").set("siteAreaHa", sqm(first(block, /land area of ([\d,.]+\s*sqm)/i)) != null ? sqm(first(block, /land area of ([\d,.]+\s*sqm)/i))! / 10_000 : null, "regex:land_area>ha"); const st = statusOf(block); if (st) r.set("status", st, "regex:status_v1"); r.set("facilityType", "hyperscale", "default:operator").set("certifications", certifications(block), "regex:certifications_v1").set("description", first(block, /ABOUT THE FACILITY[^\n]*\n\s*([^\n]{40,600})/i) ?? describe(p), "regex:about_facility"); out.push(r.done(key("pdg", code), p.url, 0.8)); } return out; }); /** Macquarie Data Centres — /data-centres//ic/ ; Intellicentre code, certifications, campus. */ facilityParser("macquarie_facility_v1", (p) => { const parts = seg(p.url); const code = parts[2]?.match(/^ic\d$/i) ? parts[2]!.toUpperCase() : null; if (!code || parts[0] !== "data-centres") return []; const r = new Rec(); const city = titleCase(parts[1]!); r.set("name", `Macquarie ${code} ${city}`, "url:slug").set("code", code, "url:slug").set("city", city, "url:slug").set("countryIso2", "AU", "default:operator"); const campus = first(p.title ?? "", /-\s*([A-Z][A-Za-z ]+Campus)/); if (campus) r.set("campusName", `Macquarie ${campus}`, "html:title"); const it = first(p.text, /(\d[\d.,]*\+?\s*MW\+?)\s*\n?\s*IT Load/i); if (it) r.set("itCapacityMw", mw(it), "regex:it_load_stat"); r.set("buildingSqm", sqm(first(p.text, /([\d,.]+\s*(?:m²|sqm|m2))\s*\n?\s*Technical space/i)), "regex:technical_space_stat").set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); if (/government|sovereign/i.test(`${p.title ?? ""} ${p.desc ?? ""}`)) r.set("facilityType", "colocation", "default:operator"); const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("macquarie", code), p.url, 0.8)]; }); /** Teraco — /data-centre-locations// ; per-facility blocks with address, white space, IT load (MW). */ facilityParser("teraco_facility_v1", (p) => { const metro = titleCase(seg(p.url).pop() ?? ""); const out: ExtractedRecord[] = []; const STOP = /^(Home|Locations?|Menu|Data Centre(?: Facility)?|Explore|Location|Key Attributes|Connectivity|Community)$/i; for (const ka of p.text.matchAll(/Key Attributes\s*\n\s*([\d,]+)\s*\n?\s*SQM White Space\s*\n\s*([\d.,]+)\s*\n?\s*IT Load/g)) { const before = p.text.slice(Math.max(0, (ka.index ?? 0) - 1800), ka.index ?? 0); const cands = [...before.matchAll(/\n\s*([A-Z]{2}\d(?:\s*\+\s*[A-Z]{2}\d)*)\s*\n\s*([A-Z][A-Za-z ]+?)(?:\s+Campus)?\s*\n/g)].filter((c) => !STOP.test(c[2]!.trim())); const m = cands[cands.length - 1]; if (!m) continue; const codes = m[1]!.split(/\s*\+\s*/); if (out.some((o) => o.key === key("teraco", codes[0]))) continue; const r = new Rec(); const site = m[2]!.trim(); r.set("name", `Teraco ${m[1]!.replace(/\s+/g, "")} ${site}`, "regex:facility_block").set("code", codes.join("+"), "regex:facility_block").set("aliases", codes.length > 1 ? codes : [], "regex:facility_block").set("campusName", `Teraco ${site} Campus`, "regex:facility_block").set("city", metro, "url:segment").set("countryIso2", "ZA", "default:operator"); r.set("buildingSqm", sqm(`${ka[1]} sqm`), "regex:white_space_stat").set("itCapacityMw", mw(`${ka[2]} MW`), "regex:it_load_stat"); const after = p.text.slice(ka.index ?? 0, (ka.index ?? 0) + 2500); const addr = first(after, /Address:\s*([^\n]+)/) ?? first(p.text, new RegExp(`${codes[0]}[^\\n]*\\n[\\s\\S]{0,600}?Address:\\s*([^\\n]+)`)); if (addr) r.set("address", addr, "regex:address_row"); const gps = after.match(/GPS Co-?ordinates?:?\s*(-?\d{1,2}\.\d{3,})[,\s]+(-?\d{1,3}\.\d{3,})/i); if (gps) r.set("lat", Number(gps[1]), "text:gps_row").set("lng", Number(gps[2]), "text:gps_row").set("geoPrecision", "exact", "text:gps_row"); const desc = first(before.slice(m.index ?? 0), /\n\s*([^\n]{40,400})\n/); r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", desc ?? describe(p), "regex:facility_block"); out.push(r.done(key("teraco", codes[0]), p.url, 0.8)); } return out; }); export const APAC_PARSERS = ["airtrunk_facility_v1", "nextdc_facility_v1", "sify_facility_v1", "digitaledge_facility_v1", "cdc_campus_v1", "pdg_facility_v1", "macquarie_facility_v1", "teraco_facility_v1"];