docs
    re-site-kit: Buildings and the registry
    v0.6.0.md

    Buildings and the registry

    Condo and townhome sites usually have a page per building. Sonor can match every MLS listing to its building, so each building page shows what's for sale there with one call. Your site tells Sonor which buildings it has by publishing a small registry, and Sonor does the matching.

    import { getBuildingListings, getBuildingSummaries } from '@sonordev/re-site-kit/server'
    import { buildingRegistry } from '@sonordev/re-site-kit'

    How it fits together

    1. Your site serves a registry at a URL of its choosing, built with buildingRegistry().
    2. You set that URL as the Building registry URL in Sonor (Real Estate, then Buildings).
    3. Sonor pulls the registry every hour, and whenever you press Refresh now there, then matches your project's listings to those buildings.
    4. Your building pages call getBuildingListings(slug), and every listing carries listing.building so a listing page can link back to its building.

    Publish the registry

    // app/api/sonor/buildings/route.ts
    import { buildingRegistry } from '@sonordev/re-site-kit'
    import { loadBuildings } from '@/lib/buildings'
    
    export const revalidate = 300
    
    export async function GET() {
      const rows = await loadBuildings()
      return Response.json(
        buildingRegistry(
          rows.map((b) => ({
            id: b.id,
            slug: b.slug,
            name: b.name,
            aliases: b.mlsNames, // other names the MLS uses for it
            addresses: b.addresses, // "100-118 Main St" is a range
            city: b.city,
            state: b.state,
            zip: b.zip,
            latitude: b.lat,
            longitude: b.lng,
            url: `https://example.com/condos/${b.slug}`,
          })),
        ),
      )
    }

    loadBuildings() stands for wherever your building pages already get their content: a CMS, a JSON file, a database. The URL must be https and on the project's own domain; Sonor won't pull a registry from anywhere else.

    buildingRegistry

    buildingRegistry(entries: BuildingRegistryEntry[]): {
      version: 1
      buildings: BuildingRegistryEntry[]
    }

    It's a pure function that tidies your rows so you can map them straight in:

    • Slugs are trimmed and lowercased, and names are trimmed.
    • An entry without a slug or a name is skipped.
    • When two entries share a slug, the first one wins.
    • Aliases and addresses are trimmed, and blanks and repeats are dropped. An alias that's the same as the name is dropped too.
    • city, state and zip are trimmed, and empty values become null.
    • latitude and longitude are kept only when they're finite numbers.
    • Every optional field comes out as null when you leave it out.

    Registry entries

    Only slug and name are required. Every address and alias you add is another way a listing finds its building.

    FieldTypeWhat it's for
    slugstringYour building's slug. It's what getBuildingListings(slug) and searchListings({ building }) take. Required.
    namestringThe building's name. Required.
    idstring | nullYour own id for it, such as a database row id. It comes back as external_id in building summaries.
    aliasesArray<string | null | undefined> | nullOther names the MLS uses for it: the subdivision or complex names agents type, in every spelling you've seen.
    addressesArray<string | null | undefined> | nullStreet addresses, one per entrance or street number. 100-118 Main St is a range, and a street name with no number (Harbor Way) means the whole street.
    city, state, zipstring | nullWhere it is.
    latitude, longitudenumber | nullIts location, for matching by proximity.
    radius_mnumber | nullHow close, in meters, a condo or townhouse listing must be to count as this building when matching by proximity. Default 40.
    urlstring | nullThe absolute URL of the building's page on your site. It comes back as listing.building.url and in building summaries.

    The route returns JSON like this:

    {
      "version": 1,
      "buildings": [
        {
          "id": "b-17",
          "slug": "harbor-point",
          "name": "Harbor Point",
          "aliases": ["Harbor Pointe Condominiums"],
          "addresses": ["100-118 Main St"],
          "city": "Springfield",
          "state": "IL",
          "zip": "62701",
          "latitude": 39.7817,
          "longitude": -89.6501,
          "radius_m": null,
          "url": "https://example.com/condos/harbor-point"
        }
      ]
    }

    How a listing is matched

    Sonor tries these rules in order, and the first one that names exactly one building wins:

    1. A decision someone made in the dashboard for that street address (assigning it to a building, or excluding it).
    2. The street address, against your registry's addresses.
    3. The MLS subdivision name, which must equal your building's name or one of its aliases. Part of a longer name never counts.
    4. For condos and townhouses, distance from your building's latitude and longitude, within radius_m.

    When a rule finds more than one building, Sonor doesn't guess. The listing goes to the review queue in the dashboard, and one click there settles every unit at that address for good. Adding addresses and aliases to the registry is how you need fewer clicks.

    A building page

    // app/condos/[slug]/page.tsx
    import { notFound } from 'next/navigation'
    import { getBuildingListings } from '@sonordev/re-site-kit/server'
    import { ListingGrid, summarizeListings } from '@sonordev/re-site-kit'
    import { loadBuilding } from '@/lib/buildings'
    
    const usd = (n: number | null) =>
      n == null
        ? null
        : new Intl.NumberFormat('en-US', {
            style: 'currency',
            currency: 'USD',
            maximumFractionDigits: 0,
          }).format(n)
    
    export default async function CondoPage({ params }: { params: Promise<{ slug: string }> }) {
      const { slug } = await params
      const building = await loadBuilding(slug) // your own content
      if (!building) notFound()
    
      const { listings } = await getBuildingListings(slug, { limit: 24, sort: 'price_asc' })
      const stats = summarizeListings(listings)
    
      return (
        <main>
          <h1>{building.name}</h1>
          {stats.count > 0 ? (
            <dl className="market-snapshot">
              <div>
                <dt>For sale</dt>
                <dd>{stats.count}</dd>
              </div>
              {stats.medianPrice != null ? (
                <div>
                  <dt>Median price</dt>
                  <dd>{usd(stats.medianPrice)}</dd>
                </div>
              ) : null}
              {stats.medianPricePerSqft != null ? (
                <div>
                  <dt>Median $/sqft</dt>
                  <dd>{usd(stats.medianPricePerSqft)}</dd>
                </div>
              ) : null}
              {stats.medianMonthlyHoa != null ? (
                <div>
                  <dt>Typical HOA</dt>
                  <dd>{usd(stats.medianMonthlyHoa)}/mo</dd>
                </div>
              ) : null}
            </dl>
          ) : null}
          <h2>For sale at {building.name}</h2>
          <ListingGrid
            listings={listings}
            hrefFor={(listing) => (listing.slug ? `/listings/${listing.slug}` : null)}
            emptyState={<p>Nothing's for sale here right now.</p>}
          />
        </main>
      )
    }

    summarizeListings is covered in Market snapshot.

    getBuildingListings

    getBuildingListings(
      slug: string,
      options?: Omit<SearchListingsOptions, 'building'>,
      fetchOptions?: { revalidate?: number },
    ): Promise<ListingSearchResult>

    The listings Sonor matched to one building, by its registry slug. It's searchListings with building set, so it takes the same filters, sort, paging and cache window (60 seconds), and returns the same { listings, pagination }. An unknown slug returns an empty page. See Search and filters for every option.

    A building's listings come from Sonor's matches in both serve modes.

    A building index

    getBuildingSummaries(
      options?: { site?: string },
      fetchOptions?: { revalidate?: number },
    ): Promise<BuildingSummary[]>

    Every building Sonor knows for the site, with how many of its listings are for sale and their price range. Buildings with nothing for sale are included with a count of 0. It's cached for 5 minutes and returns [] on any failure.

    FieldTypeWhat it is
    slugstringThe registry slug.
    namestringThe building's name.
    urlstring | nullThe building's page, from your registry.
    external_idstring | nullYour own id from the registry.
    active_countnumberActive listings matched to it.
    price_min, price_maxnumber | nullThe price range of those listings.
    // app/condos/page.tsx
    import { getBuildingSummaries } from '@sonordev/re-site-kit/server'
    
    const usd = (n: number) =>
      new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0 }).format(n)
    
    export default async function CondosPage() {
      const buildings = await getBuildingSummaries()
    
      return (
        <main>
          <h1>Condo buildings</h1>
          <ul>
            {buildings.map((b) => (
              <li key={b.slug}>
                <a href={`/condos/${b.slug}`}>{b.name}</a>
                {b.active_count > 0 ? (
                  <span>
                    {' '}
                    {b.active_count} for sale
                    {b.price_min != null && b.price_max != null
                      ? `, ${usd(b.price_min)} to ${usd(b.price_max)}`
                      : ''}
                  </span>
                ) : (
                  <span> Nothing for sale right now</span>
                )}
              </li>
            ))}
          </ul>
        </main>
      )
    }

    Linking a listing to its building

    When Sonor has matched a listing, it carries listing.building:

    interface ListingBuildingRef {
      slug: string
      name: string
      url: string | null // the building's page, from your registry
    }

    So a listing page can link back:

    {listing.building ? (
      <p>
        In <a href={listing.building.url ?? `/condos/${listing.building.slug}`}>{listing.building.name}</a>
      </p>
    ) : null}

    listing.building_name is a separate field that comes with the listing data. It isn't tied to your registry, so use listing.building for links.