/** Operators2 — EMEA facility parsers. */ import type { ExtractedRecord } from "@dci/connectors"; import { extractGeo } from "@dci/connectors"; import { countryFromText } from "@dci/core"; import { Rec, applyLdAddress, applyLdGeo, capacityMw, certifications, countryFromCity, countryOf, describe, facilityParser, first, ha, itMw, key, ldWithAddress, mw, pue, seg, sqm, statusOf, tier, titleCase } from "./shared.js"; /** OVHcloud — /en/datacenter/ index embeds a `datacenters` map {Country: [{city, country, latitude, longitude, page_url}]}. */ facilityParser("ovhcloud_locations_v1", (p) => { const i = p.html.indexOf('"datacenters":{'); if (i < 0) return []; let depth = 0, end = -1; for (let k = i + '"datacenters":'.length; k < p.html.length; k++) { const ch = p.html[k]; if (ch === "{") depth++; else if (ch === "}") { depth--; if (depth === 0) { end = k + 1; break; } } } if (end < 0) return []; let map: Record>>; try { map = JSON.parse(p.html.slice(i + '"datacenters":'.length, end)) as Record>>; } catch { return []; } const out: ExtractedRecord[] = []; for (const [country, list] of Object.entries(map)) for (const dc of list) { const city = (dc.city ?? "").trim(); if (!city) continue; const r = new Rec(); r.set("name", `OVHcloud ${city}`, "embedded-json:datacenters.city").set("city", city, "embedded-json:datacenters.city").set("countryIso2", countryFromText(dc.country ?? country), "embedded-json:datacenters.country>country"); const lat = Number(dc.latitude), lng = Number(dc.longitude); if (Number.isFinite(lat) && Number.isFinite(lng) && lat !== 0) r.set("lat", lat, "embedded-json:datacenters.latitude").set("lng", lng, "embedded-json:datacenters.longitude").set("geoPrecision", "approximate", "embedded-json:datacenters"); if (dc.page_url) r.set("website", dc.page_url.startsWith("http") ? dc.page_url : `https://www.ovhcloud.com${dc.page_url}`, "embedded-json:datacenters.page_url"); r.set("description", `OVHcloud datacenter location in ${city}, ${dc.country ?? country} (${dc.region ?? ""} region).`.replace(" ( region)", ""), "derived:index-entry"); out.push(r.done(key("ovhcloud", country, city), p.url, 0.8)); } return out; }); /** OVHcloud — city pages /en/datacenter///[/]/ (name, description). */ facilityParser("ovhcloud_facility_v1", (p) => { const parts = seg(p.url); // en, datacenter, continent, country, region, city — depth-5 pages can be regions (Arizona) or cities (Rabat) with identical H1 forms, so only depth-6 city pages are parsed; the index JSON covers every location anyway. if (parts.length < 6 || parts[1] !== "datacenter") return []; const h1 = p.h1 ?? ""; if (!/^datacent(er|re)\s+\S|datacent(er|re)$/i.test(h1) || /datacent(er|re)s\b/i.test(h1)) return []; const country = titleCase(parts[3]!); const r = new Rec(); const city = h1.replace(/\s*datacent(er|re)\s*$/i, "").replace(/^datacent(er|re)\s+/i, "").trim(); r.set("name", `OVHcloud ${city}`, "selector:h1").set("city", city, "selector:h1").set("countryIso2", countryFromText(country), "url:segment>country"); r.set("regionName", titleCase(parts[4]!), "url:segment"); r.set("description", describe(p), "meta:description").set("certifications", certifications(p.text), "regex:certifications_v1"); return [r.done(key("ovhcloud", country, city), p.url, 0.7)]; }); /** maincubes — /en/data-centers// ; "FRA 01 Frankfurt Germany", IT capacity, whitespace, design PUE. */ facilityParser("maincubes_facility_v1", (p) => { const m = p.text.match(/\b([A-Z]{3})\s?(\d{2})\s*\n?\s*([A-Z][a-zäöü]+)\s*\n?\s*(Germany|Netherlands|Austria|Switzerland|France|Belgium)\b/); const codeT = first(p.title ?? "", /\b([A-Z]{3}\d{2})\b/); const code = m ? `${m[1]}${m[2]}` : codeT; if (!code) return []; const r = new Rec(); r.set("name", `maincubes ${code}`, m ? "regex:code_city_country" : "html:title").set("code", code, "regex:code_city_country"); if (m) r.set("city", m[3], "regex:code_city_country").set("countryIso2", countryFromText(m[4]), "regex:code_city_country>country"); else { const c = countryOf(p.title, p.desc, p.text.slice(0, 2000)); if (c) r.set("countryIso2", c.iso, c.method); } r.set("itCapacityMw", itMw(p.text), "regex:it_mw_v1").set("buildingSqm", sqm(first(p.text, /([\d,.]+\s*(?:ft²|m²|sqm|sq ft))\s*\n?\s*white\s?space/i)), "regex:whitespace_stat").set("pue", pue(p.text), "regex:pue_v1").set("tier", tier(p.text), "regex:tier_v1"); const st = statusOf(p.text); if (st) r.set("status", st, "regex:status_v1"); r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); return [r.done(key("maincubes", code), p.url)]; }); /** Global Switch — /data-centres// ; .contact_offices li = "Name
NNMW
street
postal city
country". */ facilityParser("globalswitch_facility_v1", (p) => { const out: ExtractedRecord[] = []; const metro = seg(p.url).pop() ?? ""; p.$(".contact_offices li").each((_, li) => { const lines = (p.$(li).html() ?? "").split(//i).map((s) => s.replace(/<[^>]+>/g, "").replace(/&/g, "&").trim()).filter(Boolean); if (lines.length < 3) return; const r = new Rec(); const name = lines[0]!; r.set("name", `Global Switch ${name}`, "selector:.contact_offices").set("totalPowerMw", mw(lines.find((l) => /^\d[\d.]*\s*MW$/i.test(l)) ?? null), "selector:.contact_offices+mw"); const rest = lines.filter((l) => !/^\d[\d.]*\s*MW$/i.test(l)).slice(1); const countryLine = rest[rest.length - 1] ?? null; const iso = countryFromText(countryLine) ?? countryFromCity(countryLine); r.set("countryIso2", iso, "selector:.contact_offices>country"); const addrLines = iso ? rest.slice(0, -1) : rest; if (addrLines.length) { const cityLine = addrLines[addrLines.length - 1]!; const pc = cityLine.match(/^([A-Z0-9][A-Z0-9 -]{2,10})\s+([A-Za-z][A-Za-z .'-]+)$/) ?? cityLine.match(/^([A-Za-z][A-Za-z .'-]+?)\s+([A-Z0-9][A-Z0-9 -]{3,10})$/); if (pc) { const [a, b] = /^\d/.test(pc[1]!) || /\d/.test(pc[1]!) ? [pc[1]!, pc[2]!] : [pc[2]!, pc[1]!]; r.set("postalCode", a, "selector:.contact_offices").set("city", b, "selector:.contact_offices"); } else r.set("city", cityLine.replace(/\d+/g, "").trim() || titleCase(metro), "selector:.contact_offices"); r.set("address", addrLines.slice(0, -1).join(", ") || null, "selector:.contact_offices"); } if (!r.has("city")) r.set("city", titleCase(metro), "url:segment"); r.set("description", describe(p), "meta:description").set("certifications", certifications(p.text), "regex:certifications_v1"); const st = statusOf(name); if (st) r.set("status", st, "regex:status_v1"); out.push(r.done(key("globalswitch", name), p.url)); }); return out; }); /** Green Mountain — /data-center// ; "25 MW Total IT Capacity", "23 000 m2", PUE, Tier III. */ facilityParser("greenmountain_facility_v1", (p) => { const code = first(p.h1 ?? p.title ?? "", /^([A-Z]{3}-[A-Za-zøæåØÆÅ]+)$/); if (!code) return []; const r = new Rec(); r.set("name", `Green Mountain ${code}`, "selector:h1").set("code", code, "selector:h1"); const place = code.split("-")[1]!; if (!/^(east|west|north|south|central)$/i.test(place)) r.set("city", place, "selector:h1"); const c = countryOf(p.desc, p.text.slice(0, 2500)); r.set("countryIso2", c?.iso ?? "NO", c ? c.method : "default:operator"); r.set("itCapacityMw", mw(first(p.text, /Maximum IT capacity:\s*(\d[\d.,]*\+?\s*MW\+?)/i)) ?? itMw(p.text), "regex:it_mw_v1").set("buildingSqm", sqm(first(p.text, /([\d ]{3,9}\s*m2)\s*\n?\s*(?:Total Campus Footprint|\(\d)/i) ?? first(p.text, /approx ([\d ]{3,9}\s*m2)/i)), "regex:campus_footprint"); r.set("pue", pue(p.text), "regex:pue_v1").set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); if (/100 ?% renewable|hydro ?power/i.test(p.text)) r.set("renewableClaim", first(p.text, /([^.\n]*100 ?% renewable[^.\n]*)/i) ?? "Renewable (hydro) powered", "regex:renewable_claim"); const st = statusOf(`${p.desc ?? ""}\n${p.text}`, 2000); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("greenmountain", code), p.url)]; }); /** atNorth — /nordic-data-centers/-data-centers// ; FIN01 code, MW power capacity, SQM white space, PUE, Tier 3. */ facilityParser("atnorth_facility_v1", (p) => { const parts = seg(p.url); if (parts.length < 3 || parts[0] !== "nordic-data-centers") return []; const country = countryFromText(parts[1]!.replace(/-data-centers$/, "")); const code = first(`${p.title ?? ""} ${p.text.slice(0, 300)}`, /\b([A-Z]{3}\d{2})\b/); if (!code) return []; const r = new Rec(); r.set("name", `atNorth ${code} ${p.h1 ?? ""}`.trim(), "selector:h1").set("code", code, "regex:code_v1").set("countryIso2", country, "url:segment>country"); const h1city = (p.h1 ?? "").match(/^([A-Z][a-zA-Zäöåø]+)\s+(?:metro|mega)\s+site/i); if (h1city) r.set("city", h1city[1], "selector:h1"); else { const loc = p.text.slice(0, 2500).match(/in ([A-Z][a-zA-Zäöåø]+), (Finland|Denmark|Sweden|Iceland|Norway)\b/); if (loc) r.set("city", loc[1], "regex:in_city_country"); } const stat = p.text.match(/(\d[\d.,]*)\s*\n?\s*MW\s*\n?\s*(?:Site\s+)?power capacity/i); r.set("totalPowerMw", stat ? mw(`${stat[1]} MW`) : capacityMw(p.text.slice(0, 2500)), "regex:power_capacity_stat").set("buildingSqm", sqm(first(p.text, /([\d,.]+)\s*\n?\s*SQM\s*\n?\s*(?:White space|Facility size)/i)?.concat(" sqm")), "regex:whitespace_stat"); r.set("pue", pue(p.text), "regex:pue_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, 1500); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("atnorth", code), p.url)]; }); /** Bulk Infrastructure — /data-centers/locations/ ; JSON-LD LocalBusiness with address + geo, "up to 1GW" planned. */ facilityParser("bulk_facility_v1", (p) => { const o = ldWithAddress(p.html, "LocalBusiness")[0]; const code = first(p.title ?? p.h1 ?? "", /\b([A-Z]{1,2}\d{2}|OS-IX)\b/); if (!o && !code) return []; const r = new Rec(); r.set("name", `Bulk ${p.h1 ?? p.title ?? code}`.replace(/\s+in Norway|\s+in Denmark/i, ""), "selector:h1").set("code", code, "html:title"); if (o) { applyLdAddress(r, o.address); applyLdGeo(r, o); } r.set("plannedPowerMw", mw(first(p.text, /up to (\d[\d.,]*\s*[GM]W)\s*(?:of\s+)?power/i)), "regex:up_to_power").set("itCapacityMw", itMw(p.text), "regex:it_mw_v1"); r.set("siteAreaHa", ha(first(p.text, /(\d[\d,.]*\s*-?\s*hectares?)/i)), "regex:hectares_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); if (/hydro ?power|renewable/i.test(p.text)) r.set("renewableClaim", first(p.text, /([^.\n]*(?:hydro ?power|100 ?% renewable)[^.\n]*)/i), "regex:renewable_claim"); return [r.done(key("bulk", code ?? seg(p.url).pop()), p.url)]; }); /** Colt DCS — /en-GB/our-locations/data-centre-locations-/ ; data-value counters (MW, ㎡, tier). */ facilityParser("coltdcs_facility_v1", (p) => { const h1 = p.h1 ?? ""; const hasCounter = p.$("[data-value][data-suffix]").length > 0; if (!h1 || (!/data centre/i.test(`${h1} ${p.title ?? ""}`) && !hasCounter) || /get a quote|contact/i.test(h1)) return []; const hero = p.text.slice(0, 2500); const r = new Rec(); r.set("name", (/data centre/i.test(h1) ? h1 : `${h1} Data Centres`).replace(/^Colt\s+/i, "").replace(/^/, "Colt DCS ").trim(), "selector:h1"); const live = /\b(fully let|now open|in operation|(is|now|fully) operational)\b/i.test(hero); const pipeline = !live && /\b(will deliver|upon completion|on completion|due to (open|complete)|under construction|construction timeline|coming soon|scheduled to deliver|in the process of building|development of|secured land)\b/i.test(hero); p.$("[data-value][data-suffix], [data-value][data-prefix]").each((_, el) => { const v = p.$(el).attr("data-value") ?? ""; const suf = p.$(el).attr("data-suffix") ?? ""; const pre = p.$(el).attr("data-prefix") ?? ""; const label = p.$(el).parent().text().replace(v, "").replace(suf, "").replace(pre, "").replace(/\s+/g, " ").trim(); if (/MW/i.test(suf)) r.set(pipeline ? "plannedPowerMw" : "itCapacityMw", mw(`${v} MW`), "attr:data-value+MW"); else if (/㎡|m²|m2/i.test(suf)) { if (/white|technical|data hall|net|floor/i.test(label)) r.set("buildingSqm", sqm(`${v} sqm`), "attr:data-value+sqm"); else if (/site/i.test(label)) r.set("siteAreaHa", Number(v.replace(/[^\d.]/g, "")) / 10_000, "attr:data-value+sqm>ha"); else r.set("buildingSqm", sqm(`${v} sqm`), "attr:data-value+sqm"); } else if (/acre/i.test(suf) || /acre/i.test(label)) r.set("siteAreaHa", ha(`${v} acres`), "attr:data-value+acres"); else if (/tier/i.test(pre)) r.set("tier", tier(`Tier ${v}`), "attr:data-prefix+tier"); }); if (!r.has("itCapacityMw") && !r.has("plannedPowerMw")) { const v = itMw(hero); if (v) r.set(pipeline ? "plannedPowerMw" : "itCapacityMw", v, "regex:it_mw_v1"); } if (pipeline) r.set("status", /under construction|construction timeline|in the process of building|underway|being built/i.test(hero) ? "under_construction" : "announced", "regex:status_colt"); else { const st = statusOf(hero); if (st) r.set("status", st, "regex:status_v1"); } const place = first(hero, /(?:Situated|Located|Built|Based|is) in (?:the )?(?:prime location of )?([A-Z][\w'’ -]{2,40}?)(?:,|\.|\s+(?:this|and|the|our|at|an)\b)/) ?? first(hero, /\d+\s*km\s+(?:east|west|north|south)(?:-?(?:east|west))? of ([A-Z][a-zA-Z]+)/); r.set("city", place, "regex:situated_in"); const c = countryOf(p.title, p.desc, place, h1, hero); if (c) r.set("countryIso2", c.iso, c.method); if (/hyperscale/i.test(p.text.slice(0, 2000))) r.set("facilityType", "hyperscale", "regex:hyperscale"); r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); return [r.done(key("coltdcs", seg(p.url).pop()), p.url)]; }); /** Kao Data — /locations// ; campus address + "KLON-01 (8.8MW Operational)" child facilities. */ facilityParser("kaodata_facility_v1", (p) => { const campus = titleCase(seg(p.url).pop() ?? ""); if (!campus || !/data centre|campus/i.test(p.text.slice(0, 3000))) return []; const out: ExtractedRecord[] = []; const addr = p.text.match(/Address\s*\n?\s*([^\n]+?),\s*([A-Z][a-zA-Z ]+?),\s*([A-Z]{1,2}\d[\dA-Z]?\s?\d[A-Z]{2})\b/); const base = (r: Rec) => { if (addr) r.set("address", addr[1], "regex:address_row").set("city", addr[2], "regex:address_row").set("postalCode", addr[3], "regex:address_row"); else r.set("city", campus, "url:segment"); r.set("countryIso2", "GB", "default:operator"); if (/100 ?% renewable/i.test(p.text)) r.set("renewableClaim", "100% renewable energy", "regex:renewable_claim"); }; const c = new Rec(); c.set("name", `Kao Data ${campus}`, "url:segment").set("campusName", `Kao Data ${campus}`, "url:segment"); base(c); c.set("plannedPowerMw", mw(first(p.text, /(?:ITE?|IT-?)\s*load of (\d[\d.]*\+?\s*MW\+?)/i)), "regex:ite_load_full_build").set("siteAreaHa", ha(first(p.text, /(\d+\s*acres?)/i)), "regex:acres_v1").set("buildingSqm", sqm(first(p.text, /Technical Space:\s*([\d,]+\s*m2)/i)), "regex:technical_space"); c.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); out.push(c.done(key("kaodata", campus), p.url, 0.8)); for (const m of p.text.matchAll(/\b(K[A-Z]{2,4}-\d{2})\s*\((\d[\d.]*)\s*MW\s+([A-Za-z ]+?)\)/g)) { const r = new Rec(); r.set("name", `Kao Data ${m[1]}`, "regex:child_facility").set("code", m[1], "regex:child_facility").set("campusName", `Kao Data ${campus}`, "url:segment").set("itCapacityMw", mw(`${m[2]} MW`), "regex:child_facility"); r.set("status", /operational/i.test(m[3]!) ? "operational" : /development|construction/i.test(m[3]!) ? "under_construction" : /planned/i.test(m[3]!) ? "announced" : null, "regex:child_facility"); base(r); out.push(r.done(key("kaodata", m[1]), p.url, 0.85)); } return out; }); /** Pulsant — /colocation-- ; label/value technical table (IT power, building size). */ facilityParser("pulsant_facility_v1", (p) => { const h1 = p.h1 ?? ""; const m = h1.match(/^([A-Z][a-zA-Z ]+?)\s+([A-Z]{2}-\d)$/); if (!m) return []; const r = new Rec(); r.set("name", `Pulsant ${h1}`, "selector:h1").set("code", m[2], "selector:h1").set("city", m[1], "selector:h1").set("countryIso2", "GB", "default:operator"); r.set("itCapacityMw", mw(first(p.text, /Total IT power\s*\n?\s*(\d[\d.,]*\+?\s*MW\+?)/i)) ?? itMw(p.text), "regex:total_it_power_row").set("buildingSqm", sqm(first(p.text, /Total building size\s*\n?\s*([\d,.]+\s*m²)/i)), "regex:building_size_row"); const ren = first(p.text, /Renewable energy procurement\s*\n?\s*(\d+\s*%)/i); if (ren) r.set("renewableClaim", `${ren} renewable energy procurement`, "regex:renewable_row"); const cool = first(p.text, /Cooling:\s*([^\n]{2,30})/i); if (cool) r.set("coolingType", `Cooling ${cool}`, "regex:cooling_row"); r.set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); if (/edge data centre/i.test(p.text.slice(0, 1500))) r.set("facilityType", "edge", "regex:edge"); return [r.done(key("pulsant", m[2]), p.url)]; }); /** VIRTUS — /locations/// ; "LONDON4 … 10,600m2 net technical … 26.6MW of IT load". */ facilityParser("virtus_facility_v1", (p) => { const parts = seg(p.url); if (parts[0] !== "locations" || parts.length < 2) return []; const slug = parts[parts.length - 1]!; const isCampus = /campus/i.test(slug); const slugCode = slug.match(/(london|berlin|milan|frankfurt|paris)-?(\d{1,2})(?:-|$)/i); const code = isCampus ? null : slugCode ? `${slugCode[1]!.toUpperCase()}${slugCode[2]}` : first(`${p.h1 ?? ""} ${p.title ?? ""}`, /\b((?:LONDON|BERLIN|MILAN|MILANO|FRANKFURT|PARIS)\s?\d{1,2})\b/i)?.toUpperCase().replace(/\s+/g, "") ?? null; if (!code && !isCampus) return []; const r = new Rec(); const campusName = isCampus ? `VIRTUS ${titleCase(slug.replace(/-campus$/, ""))} Campus` : null; r.set("name", code ? `VIRTUS ${code}` : campusName, code ? "url:slug" : "url:slug").set("code", code, "url:slug").set("campusName", campusName, "url:slug"); const cityToken = slug.replace(/(london|berlin|milan|frankfurt|paris)-?\d{1,2}-?/gi, "").replace(/^(italy|germany|uk)-?/i, "").replace(/-?data-centres?-?/g, "").replace(/-campus$/, "").replace(/^-+|-+$/g, "").trim(); const city = cityToken ? titleCase(cityToken) : parts.length >= 3 ? titleCase(parts[2]!) : null; r.set("city", city, "url:slug"); const c = countryFromText(parts[1] === "uk" ? "united kingdom" : parts[1] === "eu" ? (parts[2] ?? "") : parts[1]!) ?? countryFromCity(city) ?? countryFromCity(code?.replace(/\d+/g, "")); r.set("countryIso2", c, "url:segment>country"); const idx = code ? p.text.search(new RegExp(`\\b${code}\\b`)) : -1; const seq = idx >= 0 ? p.text.slice(idx, idx + 3000) : p.text.slice(0, 3000); if (code) r.set("itCapacityMw", mw(first(seq, /(\d[\d.,]*\+?\s*MW\+?) of IT load/i)) ?? itMw(seq), "regex:mw_of_it_load").set("buildingSqm", sqm(first(seq, /([\d,.]+\s*m2) (?:of )?net technical/i)), "regex:net_technical_sqm"); const st = statusOf(seq, 2500); if (st) r.set("status", st, "regex:status_v1"); r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); return [r.done(key("virtus", code ?? slug), p.url, code ? 0.85 : 0.7)]; }); /** Ark Data Centres (UK) — /locations/ ; map JSON latitude/longitude, Tier 3, PUE, acres, "near ". */ facilityParser("arkdc_facility_v1", (p) => { const h1 = p.h1 ?? ""; if (!h1 || seg(p.url)[0] !== "locations") return []; const r = new Rec(); r.set("name", `Ark ${h1.replace(/\s*\(.*\)$/, "")}`, "selector:h1"); const ll = p.html.match(/"latitude"\s*:\s*"?(-?\d{1,2}\.\d{3,})"?\s*,\s*"longitude"\s*:\s*"?(-?\d{1,3}\.\d{3,})"?/); if (ll) r.set("lat", Number(ll[1]), "embedded-json:latitude").set("lng", Number(ll[2]), "embedded-json:longitude").set("geoPrecision", "parcel", "embedded-json:latitude"); const near = first(p.text, /\b(?:near|in|at|outside) ([A-Z][a-z]+(?: [A-Z][a-z]+)?)(?=[,.]| and| with| each| offering)/); r.set("city", near, "regex:near_town"); const c = countryFromText(`${h1} ${p.text.slice(0, 2000)}`) ?? countryFromCity(near) ?? countryFromCity(h1) ?? "GB"; r.set("countryIso2", c, c === "GB" && !countryFromCity(near) ? "default:operator" : "lookup:city-country"); r.set("pue", pue(p.text), "regex:pue_v1").set("tier", tier(p.text), "regex:tier_v1").set("siteAreaHa", ha(first(p.text, /(\d+\s*acres?)/i)), "regex:acres_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("arkdc", seg(p.url).pop()), p.url, 0.75)]; }); /** Telehouse — /data-centre-services//// ; name, carriers count, specs. */ facilityParser("telehouse_facility_v1", (p) => { const parts = seg(p.url); if (parts[0] !== "data-centre-services" || parts.length < 4) return []; const r = new Rec(); const name = p.h1 ?? titleCase(parts[3]!); r.set("name", /telehouse/i.test(name) ? name : `Telehouse ${name}`, "selector:h1").set("city", titleCase(parts[2]!), "url:segment"); const iso = countryFromText(parts[1] === "uk" ? "united kingdom" : parts[1]!); r.set("countryIso2", iso ?? countryFromCity(titleCase(parts[2]!)), "url:segment>country"); r.set("itCapacityMw", itMw(p.text), "regex:it_mw_v1").set("buildingSqm", sqm(first(p.text, /([\d,.]+\s*(?:m²|sqm|sq ft|square metres))\s*(?:of\s+)?(?:technical|white|data hall|net)/i)), "regex:technical_space").set("tier", tier(p.text), "regex:tier_v1"); r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); if (/carrier[- ]neutral/i.test(p.text)) r.set("facilityType", /internet exchange|LINX/i.test(p.text) ? "carrier_hotel" : "colocation", "regex:carrier_neutral"); const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1"); return [r.done(key("telehouse", parts[1], parts[2], parts[3]), p.url, 0.75)]; }); /** Africa Data Centres — one page per site (/capetown/, /lagos/, …): "CPT1 Cape Town Data Centre", MW, m². */ facilityParser("adc_facility_v1", (p) => { const m = p.text.match(/\b([A-Z]{3}\d)\s+([A-Z][a-zA-Z ]+?)\s+Data Centre\b/); if (!m) return []; const r = new Rec(); const city = m[2]!.trim(); r.set("name", `Africa Data Centres ${m[1]} ${city}`, "regex:code_city").set("code", m[1], "regex:code_city").set("city", city, "regex:code_city"); const c = countryOf(p.text.slice(0, 4000), city); if (c) r.set("countryIso2", c.iso, c.method); r.set("itCapacityMw", mw(first(p.text, /(\d[\d.]*\+?\s*MW\+?) of (?:IT\s+)?power/i)) ?? itMw(p.text), "regex:mw_of_power").set("plannedPowerMw", mw(first(p.text, /IT load of (\d[\d.]*\+?\s*MW\+?) upon completion/i) ?? first(p.text, /(?:developed|expanded) to [\d,]+ square met(?:re|er)s and (\d[\d.]*\+?\s*MW\+?)/i)), "regex:upon_completion"); r.set("buildingSqm", sqm(first(p.text, /([\d,]+\s*(?:m2|m²|square met(?:re|er)s)) of (?:IT space|secured rack space)/i)), "regex:it_space_sqm").set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description"); return [r.done(key("adc", m[1]), p.url)]; }); export const EMEA_PARSERS = ["ovhcloud_locations_v1", "ovhcloud_facility_v1", "maincubes_facility_v1", "globalswitch_facility_v1", "greenmountain_facility_v1", "atnorth_facility_v1", "bulk_facility_v1", "coltdcs_facility_v1", "kaodata_facility_v1", "pulsant_facility_v1", "virtus_facility_v1", "arkdc_facility_v1", "telehouse_facility_v1", "adc_facility_v1"]; void extractGeo;