← back to Nationalrealestate

src/ingest/listings/realtytexas.ts

69 lines

/**
 * Realty Texas adapter. Discovery: sitemap.xml → per-county `sitemap_listings_onmarket_*`
 * → listing PDP URLs (`/{city}/{slug}-id-{ID}-mls-{MLS}.html`). Facts from JSON-LD
 * `SingleFamilyResidence`+`Offer` (rich: beds/baths/floorSize). source_id = the MLS# in
 * the URL (falls back to the internal id).
 */
import type { ListingAdapter, ListingFacts } from './types.ts';
import { extractJsonLd, typeMatches, num, sqftOf, rawFetch } from './shared.ts';

const ROOT_SITEMAP = 'https://www.realtytexas.com/sitemap.xml';

export const realtytexas: ListingAdapter = {
  source: 'realtytexas',
  host: 'www.realtytexas.com',
  listingsPathHint: '/palestine/',

  async discover(cap) {
    // root is an index of per-county sitemaps; take the on-market listing ones
    const { ok, body } = await rawFetch(ROOT_SITEMAP);
    if (!ok) return [];
    const countyMaps = [...body.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)]
      .map((m) => m[1])
      .filter((u) => /sitemap_listings_onmarket_/.test(u));
    const urls: string[] = [];
    for (const cm of countyMaps) {
      if (urls.length >= cap) break;
      const leaf = await rawFetch(cm);
      if (!leaf.ok) continue;
      const locs = [...leaf.body.matchAll(/<loc>\s*([^<]+?)\s*<\/loc>/gi)]
        .map((m) => m[1])
        .filter((u) => /-mls-\d+\.html$|-id-\d+/.test(u));
      urls.push(...locs);
    }
    return urls.slice(0, cap);
  },

  parse(url, html): ListingFacts | null {
    const nodes = extractJsonLd(html);
    const res = nodes.find((n) => typeMatches(n, 'SingleFamilyResidence', 'Residence', 'House'));
    if (!res) return null;
    const addr = res.address || {};
    const geo = res.geo || {};
    const offer = res.offers || {};
    const mls = url.match(/-mls-(\d+)/);
    const iid = url.match(/-id-(\d+)/);
    const sourceId = mls?.[1] || iid?.[1];
    if (!sourceId) return null;

    const streetParts = [addr.streetAddress, addr.addressLocality, addr.addressRegion, addr.postalCode]
      .filter(Boolean).join(', ');
    const facts: ListingFacts = {
      sourceId,
      url,
      address: streetParts || res.name || null,
      city: addr.addressLocality ?? null,
      state: addr.addressRegion ?? null,
      price: num(offer.price),
      beds: num(res.numberOfBedrooms),
      baths: num(res.numberOfBathroomsTotal),
      sqft: sqftOf(res.floorSize),
      lat: geo.latitude != null ? Number(geo.latitude) : null,
      lng: geo.longitude != null ? Number(geo.longitude) : null,
      status: offer.availability?.includes('InStock') ? 'active' : 'active',
    };
    if (!facts.address && facts.price == null) return null;
    return facts;
  },
};