← back to Nationalrealestate

src/ingest/listings/coldwellbanker.ts

54 lines

/**
 * Coldwell Banker adapter. Discovery: robots → new-day listings sitemap-index → leaf
 * sitemaps → listing PDP URLs (`/{st}/{city}/{street}/lid-{ID}`). Facts from JSON-LD
 * (`Product`+`SingleFamilyResidence`+`Offer`). source_id = the `lid-{ID}` URL token.
 */
import type { ListingAdapter, ListingFacts } from './types.ts';
import { extractJsonLd, typeMatches, num, sitemapLocs } from './shared.ts';

const SITEMAP_INDEX =
  'https://www.coldwellbanker.com/xml-sitemap/states/sitemapindex-listings-new-day.xml';

export const coldwellbanker: ListingAdapter = {
  source: 'coldwellbanker',
  host: 'www.coldwellbanker.com',
  listingsPathHint: '/tx/',

  async discover(cap) {
    const locs = await sitemapLocs(SITEMAP_INDEX, 1);
    const listings = locs.filter((u) => /\/lid-[A-Za-z0-9]+/.test(u));
    return listings.slice(0, cap);
  },

  parse(url, html): ListingFacts | null {
    const nodes = extractJsonLd(html);
    const res = nodes.find((n) => typeMatches(n, 'SingleFamilyResidence', 'Residence', 'House'));
    const prod = nodes.find((n) => typeMatches(n, 'Product'));
    const addr = res?.address || prod?.offers?.address || {};
    const geo = res?.geo || {};
    const offer = prod?.offers || res?.offers || {};
    const idMatch = url.match(/\/lid-([A-Za-z0-9]+)/);
    if (!idMatch) return null;

    const streetParts = [addr.streetAddress, addr.addressLocality, addr.addressRegion, addr.postalCode]
      .filter(Boolean).join(', ');
    const facts: ListingFacts = {
      sourceId: idMatch[1],
      url,
      address: streetParts || res?.name || prod?.name || null,
      city: addr.addressLocality ?? null,
      state: addr.addressRegion ?? null,
      price: num(offer.price),
      beds: num(res?.numberOfBedrooms),
      baths: num(res?.numberOfBathroomsTotal),
      sqft: num(res?.floorSize),
      lat: geo.latitude != null ? Number(geo.latitude) : null,
      lng: geo.longitude != null ? Number(geo.longitude) : null,
      status: offer.availability?.includes('InStock') ? 'active' : null,
    };
    // fail-loud contract: a listing with neither an address nor a price is not usable
    if (!facts.address && facts.price == null) return null;
    return facts;
  },
};