SPB Git forge
38commits 1branches 0releases
338.7 MBsize
maindefault branch
6 h agolast push
HTML 53.9% TypeScript 44.5% JavaScript 0.6% SQL 0.5%
14.3 KB · 165 lines typescript
Raw Blame History
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