spb/datacenterindex
Public
HTML 53.9%
TypeScript 44.5%
JavaScript 0.6%
SQL 0.5%
1/** Operators2 — EMEA facility parsers. */2import type { ExtractedRecord } from "@dci/connectors";3import { extractGeo } from "@dci/connectors";4import { countryFromText } from "@dci/core";5import { Rec, applyLdAddress, applyLdGeo, capacityMw, certifications, countryFromCity, countryOf, describe, facilityParser, first, ha, itMw, key, ldWithAddress, mw, pue, seg, sqm, statusOf, tier, titleCase } from "./shared.js";67/** OVHcloud — /en/datacenter/ index embeds a `datacenters` map {Country: [{city, country, latitude, longitude, page_url}]}. */8facilityParser("ovhcloud_locations_v1", (p) => {9 const i = p.html.indexOf('"datacenters":{');10 if (i < 0) return [];11 let depth = 0, end = -1;12 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; } } }13 if (end < 0) return [];14 let map: Record<string, Array<Record<string, string>>>;15 try { map = JSON.parse(p.html.slice(i + '"datacenters":'.length, end)) as Record<string, Array<Record<string, string>>>; } catch { return []; }16 const out: ExtractedRecord[] = [];17 for (const [country, list] of Object.entries(map)) for (const dc of list) {18 const city = (dc.city ?? "").trim(); if (!city) continue;19 const r = new Rec();20 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");21 const lat = Number(dc.latitude), lng = Number(dc.longitude);22 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");23 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");24 r.set("description", `OVHcloud datacenter location in ${city}, ${dc.country ?? country} (${dc.region ?? ""} region).`.replace(" ( region)", ""), "derived:index-entry");25 out.push(r.done(key("ovhcloud", country, city), p.url, 0.8));26 }27 return out;28});2930/** OVHcloud — city pages /en/datacenter/<continent>/<country>/[<region>/]<city>/ (name, description). */31facilityParser("ovhcloud_facility_v1", (p) => {32 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.33 if (parts.length < 6 || parts[1] !== "datacenter") return [];34 const h1 = p.h1 ?? "";35 if (!/^datacent(er|re)\s+\S|datacent(er|re)$/i.test(h1) || /datacent(er|re)s\b/i.test(h1)) return [];36 const country = titleCase(parts[3]!);37 const r = new Rec();38 const city = h1.replace(/\s*datacent(er|re)\s*$/i, "").replace(/^datacent(er|re)\s+/i, "").trim();39 r.set("name", `OVHcloud ${city}`, "selector:h1").set("city", city, "selector:h1").set("countryIso2", countryFromText(country), "url:segment>country");40 r.set("regionName", titleCase(parts[4]!), "url:segment");41 r.set("description", describe(p), "meta:description").set("certifications", certifications(p.text), "regex:certifications_v1");42 return [r.done(key("ovhcloud", country, city), p.url, 0.7)];43});4445/** maincubes — /en/data-centers/<slug>/ ; "FRA 01 Frankfurt Germany", IT capacity, whitespace, design PUE. */46facilityParser("maincubes_facility_v1", (p) => {47 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/);48 const codeT = first(p.title ?? "", /\b([A-Z]{3}\d{2})\b/);49 const code = m ? `${m[1]}${m[2]}` : codeT;50 if (!code) return [];51 const r = new Rec();52 r.set("name", `maincubes ${code}`, m ? "regex:code_city_country" : "html:title").set("code", code, "regex:code_city_country");53 if (m) r.set("city", m[3], "regex:code_city_country").set("countryIso2", countryFromText(m[4]), "regex:code_city_country>country");54 else { const c = countryOf(p.title, p.desc, p.text.slice(0, 2000)); if (c) r.set("countryIso2", c.iso, c.method); }55 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");56 const st = statusOf(p.text); if (st) r.set("status", st, "regex:status_v1");57 r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");58 return [r.done(key("maincubes", code), p.url)];59});6061/** Global Switch — /data-centres/<city>/ ; .contact_offices li = "Name<br>NNMW<br>street<br>postal city<br>country". */62facilityParser("globalswitch_facility_v1", (p) => {63 const out: ExtractedRecord[] = [];64 const metro = seg(p.url).pop() ?? "";65 p.$(".contact_offices li").each((_, li) => {66 const lines = (p.$(li).html() ?? "").split(/<br\s*\/?>/i).map((s) => s.replace(/<[^>]+>/g, "").replace(/&/g, "&").trim()).filter(Boolean);67 if (lines.length < 3) return;68 const r = new Rec();69 const name = lines[0]!;70 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");71 const rest = lines.filter((l) => !/^\d[\d.]*\s*MW$/i.test(l)).slice(1);72 const countryLine = rest[rest.length - 1] ?? null; const iso = countryFromText(countryLine) ?? countryFromCity(countryLine);73 r.set("countryIso2", iso, "selector:.contact_offices>country");74 const addrLines = iso ? rest.slice(0, -1) : rest;75 if (addrLines.length) {76 const cityLine = addrLines[addrLines.length - 1]!;77 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})$/);78 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"); }79 else r.set("city", cityLine.replace(/\d+/g, "").trim() || titleCase(metro), "selector:.contact_offices");80 r.set("address", addrLines.slice(0, -1).join(", ") || null, "selector:.contact_offices");81 }82 if (!r.has("city")) r.set("city", titleCase(metro), "url:segment");83 r.set("description", describe(p), "meta:description").set("certifications", certifications(p.text), "regex:certifications_v1");84 const st = statusOf(name); if (st) r.set("status", st, "regex:status_v1");85 out.push(r.done(key("globalswitch", name), p.url));86 });87 return out;88});8990/** Green Mountain — /data-center/<code>/ ; "25 MW Total IT Capacity", "23 000 m2", PUE, Tier III. */91facilityParser("greenmountain_facility_v1", (p) => {92 const code = first(p.h1 ?? p.title ?? "", /^([A-Z]{3}-[A-Za-zøæåØÆÅ]+)$/);93 if (!code) return [];94 const r = new Rec();95 r.set("name", `Green Mountain ${code}`, "selector:h1").set("code", code, "selector:h1");96 const place = code.split("-")[1]!; if (!/^(east|west|north|south|central)$/i.test(place)) r.set("city", place, "selector:h1");97 const c = countryOf(p.desc, p.text.slice(0, 2500)); r.set("countryIso2", c?.iso ?? "NO", c ? c.method : "default:operator");98 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");99 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");100 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");101 const st = statusOf(`${p.desc ?? ""}\n${p.text}`, 2000); if (st) r.set("status", st, "regex:status_v1");102 return [r.done(key("greenmountain", code), p.url)];103});104105/** atNorth — /nordic-data-centers/<country>-data-centers/<site>/ ; FIN01 code, MW power capacity, SQM white space, PUE, Tier 3. */106facilityParser("atnorth_facility_v1", (p) => {107 const parts = seg(p.url);108 if (parts.length < 3 || parts[0] !== "nordic-data-centers") return [];109 const country = countryFromText(parts[1]!.replace(/-data-centers$/, ""));110 const code = first(`${p.title ?? ""} ${p.text.slice(0, 300)}`, /\b([A-Z]{3}\d{2})\b/);111 if (!code) return [];112 const r = new Rec();113 r.set("name", `atNorth ${code} ${p.h1 ?? ""}`.trim(), "selector:h1").set("code", code, "regex:code_v1").set("countryIso2", country, "url:segment>country");114 const h1city = (p.h1 ?? "").match(/^([A-Z][a-zA-Zäöåø]+)\s+(?:metro|mega)\s+site/i);115 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"); }116 const stat = p.text.match(/(\d[\d.,]*)\s*\n?\s*MW\s*\n?\s*(?:Site\s+)?power capacity/i);117 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");118 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");119 const st = statusOf(p.text, 1500); if (st) r.set("status", st, "regex:status_v1");120 return [r.done(key("atnorth", code), p.url)];121});122123/** Bulk Infrastructure — /data-centers/locations/<code> ; JSON-LD LocalBusiness with address + geo, "up to 1GW" planned. */124facilityParser("bulk_facility_v1", (p) => {125 const o = ldWithAddress(p.html, "LocalBusiness")[0];126 const code = first(p.title ?? p.h1 ?? "", /\b([A-Z]{1,2}\d{2}|OS-IX)\b/);127 if (!o && !code) return [];128 const r = new Rec();129 r.set("name", `Bulk ${p.h1 ?? p.title ?? code}`.replace(/\s+in Norway|\s+in Denmark/i, ""), "selector:h1").set("code", code, "html:title");130 if (o) { applyLdAddress(r, o.address); applyLdGeo(r, o); }131 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");132 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");133 if (/hydro ?power|renewable/i.test(p.text)) r.set("renewableClaim", first(p.text, /([^.\n]*(?:hydro ?power|100 ?% renewable)[^.\n]*)/i), "regex:renewable_claim");134 return [r.done(key("bulk", code ?? seg(p.url).pop()), p.url)];135});136137/** Colt DCS — /en-GB/our-locations/data-centre-locations-<region>/<slug> ; data-value counters (MW, ㎡, tier). */138facilityParser("coltdcs_facility_v1", (p) => {139 const h1 = p.h1 ?? "";140 const hasCounter = p.$("[data-value][data-suffix]").length > 0;141 if (!h1 || (!/data centre/i.test(`${h1} ${p.title ?? ""}`) && !hasCounter) || /get a quote|contact/i.test(h1)) return [];142 const hero = p.text.slice(0, 2500);143 const r = new Rec();144 r.set("name", (/data centre/i.test(h1) ? h1 : `${h1} Data Centres`).replace(/^Colt\s+/i, "").replace(/^/, "Colt DCS ").trim(), "selector:h1");145 const live = /\b(fully let|now open|in operation|(is|now|fully) operational)\b/i.test(hero);146 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);147 p.$("[data-value][data-suffix], [data-value][data-prefix]").each((_, el) => {148 const v = p.$(el).attr("data-value") ?? ""; const suf = p.$(el).attr("data-suffix") ?? ""; const pre = p.$(el).attr("data-prefix") ?? "";149 const label = p.$(el).parent().text().replace(v, "").replace(suf, "").replace(pre, "").replace(/\s+/g, " ").trim();150 if (/MW/i.test(suf)) r.set(pipeline ? "plannedPowerMw" : "itCapacityMw", mw(`${v} MW`), "attr:data-value+MW");151 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"); }152 else if (/acre/i.test(suf) || /acre/i.test(label)) r.set("siteAreaHa", ha(`${v} acres`), "attr:data-value+acres");153 else if (/tier/i.test(pre)) r.set("tier", tier(`Tier ${v}`), "attr:data-prefix+tier");154 });155 if (!r.has("itCapacityMw") && !r.has("plannedPowerMw")) { const v = itMw(hero); if (v) r.set(pipeline ? "plannedPowerMw" : "itCapacityMw", v, "regex:it_mw_v1"); }156 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"); }157 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]+)/);158 r.set("city", place, "regex:situated_in");159 const c = countryOf(p.title, p.desc, place, h1, hero); if (c) r.set("countryIso2", c.iso, c.method);160 if (/hyperscale/i.test(p.text.slice(0, 2000))) r.set("facilityType", "hyperscale", "regex:hyperscale");161 r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");162 return [r.done(key("coltdcs", seg(p.url).pop()), p.url)];163});164165/** Kao Data — /locations/<campus>/ ; campus address + "KLON-01 (8.8MW Operational)" child facilities. */166facilityParser("kaodata_facility_v1", (p) => {167 const campus = titleCase(seg(p.url).pop() ?? "");168 if (!campus || !/data centre|campus/i.test(p.text.slice(0, 3000))) return [];169 const out: ExtractedRecord[] = [];170 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/);171 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"); };172 const c = new Rec();173 c.set("name", `Kao Data ${campus}`, "url:segment").set("campusName", `Kao Data ${campus}`, "url:segment"); base(c);174 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");175 c.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");176 out.push(c.done(key("kaodata", campus), p.url, 0.8));177 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)) {178 const r = new Rec();179 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");180 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");181 base(r);182 out.push(r.done(key("kaodata", m[1]), p.url, 0.85));183 }184 return out;185});186187/** Pulsant — /colocation-<city>-<code|datacentre> ; label/value technical table (IT power, building size). */188facilityParser("pulsant_facility_v1", (p) => {189 const h1 = p.h1 ?? "";190 const m = h1.match(/^([A-Z][a-zA-Z ]+?)\s+([A-Z]{2}-\d)$/);191 if (!m) return [];192 const r = new Rec();193 r.set("name", `Pulsant ${h1}`, "selector:h1").set("code", m[2], "selector:h1").set("city", m[1], "selector:h1").set("countryIso2", "GB", "default:operator");194 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");195 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");196 const cool = first(p.text, /Cooling:\s*([^\n]{2,30})/i); if (cool) r.set("coolingType", `Cooling ${cool}`, "regex:cooling_row");197 r.set("tier", tier(p.text), "regex:tier_v1").set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");198 if (/edge data centre/i.test(p.text.slice(0, 1500))) r.set("facilityType", "edge", "regex:edge");199 return [r.done(key("pulsant", m[2]), p.url)];200});201202/** VIRTUS — /locations/<country>/<metro>/<slug> ; "LONDON4 … 10,600m2 net technical … 26.6MW of IT load". */203facilityParser("virtus_facility_v1", (p) => {204 const parts = seg(p.url);205 if (parts[0] !== "locations" || parts.length < 2) return [];206 const slug = parts[parts.length - 1]!;207 const isCampus = /campus/i.test(slug);208 const slugCode = slug.match(/(london|berlin|milan|frankfurt|paris)-?(\d{1,2})(?:-|$)/i);209 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;210 if (!code && !isCampus) return [];211 const r = new Rec();212 const campusName = isCampus ? `VIRTUS ${titleCase(slug.replace(/-campus$/, ""))} Campus` : null;213 r.set("name", code ? `VIRTUS ${code}` : campusName, code ? "url:slug" : "url:slug").set("code", code, "url:slug").set("campusName", campusName, "url:slug");214 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();215 const city = cityToken ? titleCase(cityToken) : parts.length >= 3 ? titleCase(parts[2]!) : null;216 r.set("city", city, "url:slug");217 const c = countryFromText(parts[1] === "uk" ? "united kingdom" : parts[1] === "eu" ? (parts[2] ?? "") : parts[1]!) ?? countryFromCity(city) ?? countryFromCity(code?.replace(/\d+/g, ""));218 r.set("countryIso2", c, "url:segment>country");219 const idx = code ? p.text.search(new RegExp(`\\b${code}\\b`)) : -1;220 const seq = idx >= 0 ? p.text.slice(idx, idx + 3000) : p.text.slice(0, 3000);221 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");222 const st = statusOf(seq, 2500); if (st) r.set("status", st, "regex:status_v1");223 r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");224 return [r.done(key("virtus", code ?? slug), p.url, code ? 0.85 : 0.7)];225});226227/** Ark Data Centres (UK) — /locations/<slug> ; map JSON latitude/longitude, Tier 3, PUE, acres, "near <town>". */228facilityParser("arkdc_facility_v1", (p) => {229 const h1 = p.h1 ?? "";230 if (!h1 || seg(p.url)[0] !== "locations") return [];231 const r = new Rec();232 r.set("name", `Ark ${h1.replace(/\s*\(.*\)$/, "")}`, "selector:h1");233 const ll = p.html.match(/"latitude"\s*:\s*"?(-?\d{1,2}\.\d{3,})"?\s*,\s*"longitude"\s*:\s*"?(-?\d{1,3}\.\d{3,})"?/);234 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");235 const near = first(p.text, /\b(?:near|in|at|outside) ([A-Z][a-z]+(?: [A-Z][a-z]+)?)(?=[,.]| and| with| each| offering)/);236 r.set("city", near, "regex:near_town");237 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");238 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");239 const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1");240 return [r.done(key("arkdc", seg(p.url).pop()), p.url, 0.75)];241});242243/** Telehouse — /data-centre-services/<country>/<city>/<facility>/ ; name, carriers count, specs. */244facilityParser("telehouse_facility_v1", (p) => {245 const parts = seg(p.url);246 if (parts[0] !== "data-centre-services" || parts.length < 4) return [];247 const r = new Rec();248 const name = p.h1 ?? titleCase(parts[3]!);249 r.set("name", /telehouse/i.test(name) ? name : `Telehouse ${name}`, "selector:h1").set("city", titleCase(parts[2]!), "url:segment");250 const iso = countryFromText(parts[1] === "uk" ? "united kingdom" : parts[1]!); r.set("countryIso2", iso ?? countryFromCity(titleCase(parts[2]!)), "url:segment>country");251 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");252 r.set("certifications", certifications(p.text), "regex:certifications_v1").set("description", describe(p), "meta:description");253 if (/carrier[- ]neutral/i.test(p.text)) r.set("facilityType", /internet exchange|LINX/i.test(p.text) ? "carrier_hotel" : "colocation", "regex:carrier_neutral");254 const st = statusOf(p.text.slice(0, 3000)); if (st) r.set("status", st, "regex:status_v1");255 return [r.done(key("telehouse", parts[1], parts[2], parts[3]), p.url, 0.75)];256});257258/** Africa Data Centres — one page per site (/capetown/, /lagos/, …): "CPT1 Cape Town Data Centre", MW, m². */259facilityParser("adc_facility_v1", (p) => {260 const m = p.text.match(/\b([A-Z]{3}\d)\s+([A-Z][a-zA-Z ]+?)\s+Data Centre\b/);261 if (!m) return [];262 const r = new Rec();263 const city = m[2]!.trim();264 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");265 const c = countryOf(p.text.slice(0, 4000), city); if (c) r.set("countryIso2", c.iso, c.method);266 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");267 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");268 return [r.done(key("adc", m[1]), p.url)];269});270271export 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"];272void extractGeo;273