spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/** Operators2 — Asia-Pacific facility parsers. */2import type { ExtractedRecord } from "@dci/connectors";3import { countryFromText } from "@dci/core";4import { Rec, capacityMw, certifications, countryFromCity, countryOf, describe, facilityParser, first, ha, itMw, key, mw, pue, seg, sqm, statusOf, tier, titleCase } from "./shared.js";56/** AirTrunk — /location/<code>-<city>/ ; "Key features" list: capacity, size (ha, m2), design PUE. */7facilityParser("airtrunk_facility_v1", (p) => {8 const h1 = p.h1 ?? "";9 const m = h1.match(/^([A-Z]{3}\d)\s+(.+)$/);10 if (!m || /\/(zh-hans|zh|ja|ko)\//.test(p.url)) return [];11 const r = new Rec();12 const city = m[2]!.trim();13 r.set("name", `AirTrunk ${h1}`, "selector:h1").set("code", m[1], "selector:h1").set("city", city, "selector:h1");14 const c = countryOf(city, p.text.slice(0, 1500)); if (c) r.set("countryIso2", c.iso, c.method);15 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");16 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");17 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");18 const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1");19 return [r.done(key("airtrunk", m[1]), p.url)];20});2122/** NEXTDC — /data-centres/<metro>-data-centres/<code>-<city> ; IT capacity ("12MW", "20MW+", "10+MW"), technical space, rack capacity, Tier III. */23facilityParser("nextdc_facility_v1", (p) => {24 const slug = seg(p.url).pop() ?? "";25 const m = slug.match(/^([a-z]{1,2}\d{1,2})-([a-z-]+)$/);26 if (!m) return [];27 const code = m[1]!.toUpperCase();28 const r = new Rec();29 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");30 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");31 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");32 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/);33 if (addr) r.set("address", addr[1], "regex:au_address").set("regionName", addr[3], "regex:au_address").set("postalCode", addr[4], "regex:au_address");34 r.set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");35 const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1");36 return [r.done(key("nextdc", code), p.url)];37});3839/** Sify — /data-center/<city>NN-data-center/ ; IT power, Rated 3, certifications. HQ JSON-LD is ignored on purpose. */40facilityParser("sify_facility_v1", (p) => {41 const slug = seg(p.url).pop() ?? "";42 const m = slug.match(/^([a-z]+?)(\d{2})?-data-center$/);43 if (!m) return [];44 const city = titleCase(m[1]!);45 const code = `${city} ${m[2] ?? "01"}`;46 const r = new Rec();47 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");48 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");49 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, "");50 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");51 if (/AI[- ]ready|hyperscale/i.test(`${p.title ?? ""} ${p.desc ?? ""}`)) r.set("isAi", /AI[- ]ready/i.test(`${p.title ?? ""} ${p.desc ?? ""}`), "regex:ai_ready");52 return [r.done(key("sify", code), p.url, 0.8)];53});5455/** Digital Edge — /resources/data-center/<code>/ ; "<CODE> <City> Data Center", campus MW at full capacity. */56facilityParser("digitaledge_facility_v1", (p) => {57 const parts = seg(p.url);58 if (parts.includes("cn") || parts.includes("jp")) return [];59 const code = (p.h1 ?? "").trim();60 if (!/^[A-Z]{3,5}\d$/.test(code)) return [];61 const r = new Rec();62 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/);63 r.set("name", `Digital Edge ${code}${city ? ` ${city}` : ""}`, "selector:h1").set("code", code, "selector:h1").set("city", city, "regex:code_city");64 const c = countryOf(p.desc, city, p.text.slice(0, 2000)); if (c) r.set("countryIso2", c.iso, c.method);65 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");66 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");67 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");68 return [r.done(key("digitaledge", code), p.url)];69});7071/** CDC Data Centres — /locations/<city>/ ; campus blocks "Hume Campus One … Current capacity (operating) 21 MW". */72facilityParser("cdc_campus_v1", (p) => {73 const city = p.h1 ?? titleCase(seg(p.url).pop() ?? "");74 const out: ExtractedRecord[] = [];75 const iso = countryFromCity(city) ?? "AU";76 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]!));77 for (let i = 0; i < heads.length; i++) {78 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);79 const block = p.text.slice(start, end).split(/\n\s*(?:Get in touch|Book a tour|Learn more)\s*\n/)[0] ?? "";80 const campus = h[1]!.trim();81 if (out.some((o) => o.key === key("cdc", city, campus))) continue;82 const r = new Rec();83 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");84 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);85 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);86 r.set("totalPowerMw", mw(operating), "regex:campus_block").set("plannedPowerMw", mw(planned), "regex:campus_block");87 if (!operating && planned) r.set("status", /under construction|early stages of construction/i.test(block) ? "under_construction" : "announced", "regex:campus_block");88 else if (/under construction/i.test(block) && !/operational/i.test(block)) r.set("status", "under_construction", "regex:campus_block");89 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"); }90 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");91 const blurb = block.split("\n").map((l) => l.trim()).find((l) => l.length > 30);92 r.set("facilityType", "hyperscale", "default:operator").set("description", blurb ?? describe(p), "regex:campus_block").set("certifications", certifications(p.text), "regex:certifications_v1");93 if (r.has("totalPowerMw") || r.has("plannedPowerMw") || codes.length) out.push(r.done(key("cdc", city, campus), p.url, 0.8));94 }95 return out;96});9798/** Princeton Digital Group — /locations/<country>/ ; "<City> - <CODE>" sections with Colocation Area and Capacity. */99facilityParser("pdg_facility_v1", (p) => {100 const parts = seg(p.url);101 if (parts[0] !== "locations" || parts.length < 2) return [];102 const out: ExtractedRecord[] = [];103 const countryFromUrl = countryFromText(parts[1]!.replace(/-/g, " "));104 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)];105 for (let i = 0; i < heads.length; i++) {106 const h = heads[i]!; const start = h.index ?? 0;107 const next = heads.slice(i + 1).find((x) => x[2] !== h[2]);108 const block = p.text.slice(start, next?.index ?? p.text.length);109 const city = h[1]!.trim(), code = h[2]!;110 if (out.some((o) => o.key === key("pdg", code))) continue;111 const r = new Rec();112 r.set("name", `PDG ${code} ${city}`, "regex:city_code_heading").set("code", code, "regex:city_code_heading").set("city", city, "regex:city_code_heading");113 const c = countryFromCity(city) ?? countryFromUrl; r.set("countryIso2", c, countryFromCity(city) ? "lookup:city-country" : "url:segment>country");114 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");115 const st = statusOf(block); if (st) r.set("status", st, "regex:status_v1");116 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");117 out.push(r.done(key("pdg", code), p.url, 0.8));118 }119 return out;120});121122/** Macquarie Data Centres — /data-centres/<city>/ic<N>/ ; Intellicentre code, certifications, campus. */123facilityParser("macquarie_facility_v1", (p) => {124 const parts = seg(p.url);125 const code = parts[2]?.match(/^ic\d$/i) ? parts[2]!.toUpperCase() : null;126 if (!code || parts[0] !== "data-centres") return [];127 const r = new Rec();128 const city = titleCase(parts[1]!);129 r.set("name", `Macquarie ${code} ${city}`, "url:slug").set("code", code, "url:slug").set("city", city, "url:slug").set("countryIso2", "AU", "default:operator");130 const campus = first(p.title ?? "", /-\s*([A-Z][A-Za-z ]+Campus)/); if (campus) r.set("campusName", `Macquarie ${campus}`, "html:title");131 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");132 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");133 if (/government|sovereign/i.test(`${p.title ?? ""} ${p.desc ?? ""}`)) r.set("facilityType", "colocation", "default:operator");134 const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1");135 return [r.done(key("macquarie", code), p.url, 0.8)];136});137138/** Teraco — /data-centre-locations/<metro>/ ; per-facility blocks with address, white space, IT load (MW). */139facilityParser("teraco_facility_v1", (p) => {140 const metro = titleCase(seg(p.url).pop() ?? "");141 const out: ExtractedRecord[] = [];142 const STOP = /^(Home|Locations?|Menu|Data Centre(?: Facility)?|Explore|Location|Key Attributes|Connectivity|Community)$/i;143 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)) {144 const before = p.text.slice(Math.max(0, (ka.index ?? 0) - 1800), ka.index ?? 0);145 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()));146 const m = cands[cands.length - 1];147 if (!m) continue;148 const codes = m[1]!.split(/\s*\+\s*/);149 if (out.some((o) => o.key === key("teraco", codes[0]))) continue;150 const r = new Rec();151 const site = m[2]!.trim();152 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");153 r.set("buildingSqm", sqm(`${ka[1]} sqm`), "regex:white_space_stat").set("itCapacityMw", mw(`${ka[2]} MW`), "regex:it_load_stat");154 const after = p.text.slice(ka.index ?? 0, (ka.index ?? 0) + 2500);155 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");156 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");157 const desc = first(before.slice(m.index ?? 0), /\n\s*([^\n]{40,400})\n/);158 r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", desc ?? describe(p), "regex:facility_block");159 out.push(r.done(key("teraco", codes[0]), p.url, 0.8));160 }161 return out;162});163164export 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"];165