Navigate directly to a Gilbert Group Real Estate property page from a known property identifier, open the external Buildout listing page, verify it loaded, and extract the property's available demographics and broker contact information.
Use Cases
Use when the caller provides a Gilbert Group property identifier, such as {property-id}, and needs the corresponding listing, demographic metrics, or contact details inspected.
Automation Flow
- Build the direct listing URL:
https://gilbertgrouprealestate.com/property-listings/?propertyId={property-id}-lease#search_listings/. - In one browser-agent call,
gotothat URL, wait up to 8 seconds foriframe[src*="buildout.com"], then evaluate the following script to navigate to the preferred external Buildout frame:
(() => {
const frames = [...document.querySelectorAll("iframe[src]")].filter((f) => {
try {
return new URL(f.src).host !== location.host;
} catch {
return false;
}
});
const preferred = frames.filter((f) => f.src.includes("buildout.com"));
const pool = preferred.length ? preferred : frames;
pool.sort(
(a, b) => b.clientWidth * b.clientHeight - a.clientWidth * a.clientHeight,
);
if (!pool.length) return { navigated: false, reason: "no-external-listing-frame" };
window.location.href = pool[0].src;
return { navigated: true, frameUrl: pool[0].src };
})();- Wait for
h1for up to 10 seconds, then run this self-contained extractor on the loaded Buildout page:
(() => {
const clean = (value) => (value ? value.replace(/\s+/g, " ").trim() : null);
const table = document.querySelector(
'[slug="demographics"] table, .pdt-demographics table',
);
const output = {
url: location.href,
title: clean(document.title),
heading: clean(document.querySelector("h1")?.textContent),
loaded: !!document.querySelector("h1"),
demographics: [],
contacts: [],
};
if (table) {
const headers = [...table.querySelectorAll("thead th")].map((th) =>
clean(th.textContent),
);
const radii = headers
.map((header) => {
const match = /(?:^|\b)(\d+(?:\.\d+)?)\s*Mile/i.exec(header || "");
return match ? Number(match[1]) : null;
})
.filter((radius) => radius !== null);
output.demographics = radii.map((radiusMiles) => ({ radiusMiles }));
[...table.querySelectorAll("tbody tr")].forEach((row) => {
const cells = [...row.querySelectorAll("td")];
if (cells.length < 2) return;
const label = clean(cells[0].textContent)?.toLowerCase() || "";
const values = cells.slice(1).map((td) => clean(td.textContent));
radii.forEach((radiusMiles, index) => {
const item = output.demographics[index];
const raw = values[index];
if (!raw || /^0(?:\.0)?%?$/.test(raw)) return;
if (/total households/i.test(label))
item.households = parseInt(raw.replace(/[^0-9]/g, ""), 10) || null;
else if (/total population/i.test(label))
item.population = parseInt(raw.replace(/[^0-9]/g, ""), 10) || null;
else if (/average household income/i.test(label))
item.avgHHIncome = parseFloat(raw.replace(/[^0-9.]/g, "")) || null;
else if (/per capita income/i.test(label))
item.perCapitaIncome = parseFloat(raw.replace(/[^0-9.]/g, "")) || null;
});
});
}
const cards = [...document.querySelectorAll(".pdt-broker, .broker-info")];
const seen = new Set();
cards.forEach((card) => {
const key = clean(card.innerText);
if (!key || seen.has(key)) return;
seen.add(key);
output.contacts.push({
name: clean(
card.querySelector('.broker-name, [class*="broker-name"], h3, h4, strong')
?.textContent,
),
title: clean(
card.querySelector('.broker-title, [class*="broker-title"], .title')
?.textContent,
),
phones: [...card.querySelectorAll('a[href^="tel:"]')]
.map((a) => clean(a.textContent))
.filter(Boolean),
email: clean(card.querySelector('a[href^="mailto:"]')?.textContent),
license: clean(
card.querySelector('.broker-license, [class*="license"]')?.textContent,
),
img: card.querySelector("img")?.src || null,
text: key,
});
});
if (!output.contacts.length) {
const fallback = [
...document.querySelectorAll(
'.agent-info, [slug*="broker"], [slug*="contact"], [slug*="agent"], .contact-info, .broker-card, .js-broker, .pdt-contacts',
),
];
fallback.forEach((card) => {
const text = clean(card.innerText);
if (!text || seen.has(text)) return;
seen.add(text);
output.contacts.push({
name: clean(card.querySelector("h3, h4, strong")?.textContent),
title: clean(card.querySelector('.title, [class*="title"]')?.textContent),
phones: [...card.querySelectorAll('a[href^="tel:"]')]
.map((a) => clean(a.textContent))
.filter(Boolean),
email: clean(card.querySelector('a[href^="mailto:"]')?.textContent),
license: null,
img: card.querySelector("img")?.src || null,
text,
});
});
}
return output;
})();Possible Friction Points
- The target route is a single-page property listing shell keyed by the query parameter
propertyId; the observed listing form appends-leaseto the property identifier. - Retain the
#search_listings/fragment in the direct route. - The useful property content is embedded in an external Buildout iframe. Prefer an iframe whose
srccontainsbuildout.com; otherwise select the largest external iframe and navigate to itssrc. - Because the Buildout frame is external, navigate to its URL before extracting its DOM rather than assuming the parent page can query the frame contents.
- Do not guess an opaque property identifier. Substitute the caller-provided
{property-id}into the URL. - Demographics may be absent. The extractor returns
demographics: []when no matching table exists. - Broker markup may use
.pdt-brokeror.broker-info; if neither exists, the extractor checks agent, contact, broker, andpdt-contactsvariants and returns raw card text as a fallback. - Contact cards may contain duplicate broker representations; the extractor deduplicates them by normalized visible text.
- Demographic table selectors rely on the Buildout
slug="demographics"convention or the.pdt-demographicsclass; verify these selectors if Buildout changes its markup.