diff --git a/makelaars.md b/makelaars.md
index 0e4a09e..8685220 100644
--- a/makelaars.md
+++ b/makelaars.md
@@ -33,9 +33,9 @@
| [x] | 3D Makelaars | 3dmakelaars.nl | Gerrit Verboonstraat 17 |
| [x] | Dupont Makelaars | dupont.nl | Rotterdamsedijk 437 |
| [x] | D&S Makelaardij | densmakelaars.nl | Land van Belofte 50 |
-| [ ] | Moerman & De Jong Makelaars | moerman-dejong.nl | Lange Kerkstraat 80B |
+| [x] | Moerman & De Jong Makelaars | moerman-dejong.nl | Lange Kerkstraat 80B |
| [ ] | Hagestein Makelaardij | — | Degerfors 54 |
-| [ ] | Schieland Borsboom NVM Makelaars | schielandborsboom.nl | (Rotterdam, actief in Schiedam) |
+| [x] | Schieland Borsboom NVM Makelaars | schielandborsboom.nl | (Rotterdam, actief in Schiedam) |
## Leiden
diff --git a/src/adapters/api.py b/src/adapters/api.py
index 274e69c..450d003 100644
--- a/src/adapters/api.py
+++ b/src/adapters/api.py
@@ -182,11 +182,74 @@ def fetch_ooms() -> list[RawListing]:
log.info("ooms: %d listings opgehaald", len(listings))
return listings
+# ---------------------------------------------------------------------------
+# Moerman & De Jong Makelaars (Schiedam)
+# ---------------------------------------------------------------------------
+# Zelfde OG Online / realtime-listings platform als Bjornd.
+
+_MOERMAN_BASE = "https://www.moerman-dejong.nl"
+_MOERMAN_SKIP = {"rented", "rented_ur"}
+
+_MOERMAN_STATUS_MAP = {
+ "available": "beschikbaar",
+ "under_bid": "onder_bod",
+ "under_option": "onder_bod",
+ "sold": "verkocht",
+ "sold_ur": "verkocht",
+}
+
+
+def fetch_moerman() -> list[RawListing]:
+ data = fetch_json(
+ f"{_MOERMAN_BASE}/nl/realtime-listings/consumer",
+ headers={"X-Requested-With": "XMLHttpRequest"},
+ )
+
+ listings = []
+ for item in data:
+ if not item.get("isSales"):
+ continue
+ if item.get("statusOrig") in _MOERMAN_SKIP:
+ continue
+ if item.get("salesPrice", 0) > config.MAX_PRICE:
+ continue
+
+ postcode = (item.get("zipcode") or "").replace(" ", "") or None
+ perceel = item.get("plotSurface") or None
+ if perceel == 0:
+ perceel = None
+
+ raw_year = item.get("dateOfConstruction") or ""
+ bouwjaar = int(raw_year) if raw_year.isdigit() else None
+
+ listings.append(RawListing(
+ url=_MOERMAN_BASE + item["url"],
+ source_makelaar="moerman",
+ status=_MOERMAN_STATUS_MAP.get(item.get("statusOrig", ""), "beschikbaar"),
+ adres=item.get("address") or None,
+ postcode=postcode,
+ stad=item.get("city") or None,
+ prijs=item.get("salesPrice") or None,
+ woningtype=item.get("type") or None,
+ woonoppervlak=item.get("livingSurface") or None,
+ perceeloppervlak=perceel,
+ kamers=item.get("rooms") or None,
+ slaapkamers=item.get("bedrooms") or None,
+ bouwjaar=bouwjaar,
+ energielabel=item.get("energyLabel") or None,
+ hero_image_url=item.get("photo") or None,
+ ))
+
+ log.info("moerman: %d koopwoningen opgehaald", len(listings))
+ return listings
+
+
# ---------------------------------------------------------------------------
# SCRAPERS — exporteer hier alle actieve API adapters
# ---------------------------------------------------------------------------
-
+
SCRAPERS = {
'bjornd': fetch_bjornd,
'ooms': fetch_ooms,
+ 'moerman': fetch_moerman,
}
diff --git a/src/adapters/ssr.py b/src/adapters/ssr.py
index 0fe4c4b..1f24630 100644
--- a/src/adapters/ssr.py
+++ b/src/adapters/ssr.py
@@ -870,6 +870,174 @@ def fetch_dupont() -> list[RawListing]:
return listings
+# ---------------------------------------------------------------------------
+# Schieland Borsboom NVM Makelaars (Rotterdam, actief in Schiedam)
+# ---------------------------------------------------------------------------
+
+_SCHIELAND_BASE = "https://www.schielandborsboom.nl"
+
+_SCHIELAND_STATUS_MAP = {
+ "beschikbaar": "beschikbaar",
+ "onder bod": "onder_bod",
+ "onder optie": "onder_bod",
+ "verkocht o.v.": "verkocht",
+ "verkocht": "verkocht",
+}
+
+
+def _schieland_detail(detail_url: str) -> dict:
+ """Fetch Schieland Borsboom detail page and extract kenmerken."""
+ try:
+ soup = fetch_soup(detail_url)
+
+ # Postcode from house__status p (e.g. "3117 DP Schiedam")
+ postcode_el = soup.select_one("div.house__status p")
+ postcode = _extract_postcode(postcode_el.get_text()) if postcode_el else None
+
+ # Parse #kenmerken section:
labelvalue
+ kv: dict[str, str] = {}
+ kenmerken = soup.select_one("#kenmerken")
+ if kenmerken:
+ for li in kenmerken.select("li"):
+ label_el = li.select_one("strong")
+ value_el = li.select_one("span")
+ if label_el and value_el:
+ # Strip nested links (e.g. "Hypotheek berekenen")
+ for a in value_el.select("a"):
+ a.decompose()
+ kv[label_el.get_text(strip=True).lower()] = value_el.get_text(strip=True)
+
+ return {
+ "postcode": postcode,
+ "status": kv.get("status", "").lower(),
+ "woningtype": kv.get("soort bouw"),
+ "bouwjaar": kv.get("bouwjaar"),
+ "woonoppervlak": kv.get("woonoppervlakte"),
+ "perceeloppervlak": kv.get("perceeloppervlakte"),
+ "kamers": kv.get("aantal kamers"),
+ "slaapkamers": kv.get("aantal slaapkamers"),
+ "energielabel": kv.get("energielabel"),
+ }
+ except Exception as e:
+ log.warning("schielandborsboom: detail fetch fout %s: %s", detail_url, e)
+ return {}
+
+
+def fetch_schielandborsboom() -> list[RawListing]:
+ """Fetch Schieland Borsboom NVM listings (koop only, Schiedam)."""
+ listings = []
+ page = 1
+
+ while True:
+ if page == 1:
+ url = f"{_SCHIELAND_BASE}/wonen?sure_koop_huur=koop"
+ else:
+ url = f"{_SCHIELAND_BASE}/wonen/page/{page}/?sure_koop_huur=koop"
+
+ soup = fetch_soup(url)
+ cards = soup.select("div.card.card--house")
+ if not cards:
+ break
+
+ for card in cards:
+ try:
+ a_tag = card.select_one("a.card__anchor")
+ if not a_tag or "href" not in a_tag.attrs:
+ continue
+ detail_url = a_tag["href"]
+ if not detail_url.startswith("http"):
+ detail_url = _SCHIELAND_BASE + detail_url
+
+ # Filter: only Schiedam
+ stad_el = card.select_one("p.house-place")
+ stad = stad_el.get_text(strip=True) if stad_el else None
+ if not stad or stad.lower() != "schiedam":
+ continue
+
+ # Status from card-house__thumb second class
+ thumb = card.select_one("div.card-house__thumb")
+ status_classes = thumb.get("class", []) if thumb else []
+ status_text = next(
+ (c for c in status_classes if c != "card-house__thumb"), "beschikbaar"
+ ).lower()
+ status = _SCHIELAND_STATUS_MAP.get(status_text, "beschikbaar")
+
+ # Price
+ prijs = parse_prijs(_text(card, "p.price"))
+ if prijs and prijs > config.MAX_PRICE:
+ continue
+
+ adres = _text(card, "h4.house-street")
+
+ # Hero image from picture source (medium size)
+ src_tag = card.select_one('picture source[media="(min-width:100px)"]')
+ hero = src_tag["srcset"] if src_tag else _src(card, "img")
+ if hero and not hero.startswith("http"):
+ hero = _SCHIELAND_BASE + hero
+
+ # Data icons on card: surface, bedrooms, energy label
+ woonoppervlak_card = None
+ slaapkamers_card = None
+ energielabel_card = None
+ for data_div in card.select("div.data"):
+ txt = data_div.get_text(strip=True)
+ if data_div.select_one("i.icon-surface"):
+ woonoppervlak_card = parse_m2(txt)
+ elif data_div.select_one("i.icon-bedrooms"):
+ m = re.search(r"(\d+)", txt)
+ slaapkamers_card = int(m.group(1)) if m else None
+ elif data_div.select_one("i.icon-label"):
+ energielabel_card = txt.strip() or None
+
+ # Fetch detail page for full kenmerken
+ kk = _schieland_detail(detail_url)
+
+ # Refine status from detail page
+ if kk.get("status"):
+ status = _SCHIELAND_STATUS_MAP.get(kk["status"], status)
+
+ # Parse kamers: "5 kamers" → 5
+ kamers = None
+ if kk.get("kamers"):
+ m = re.search(r"(\d+)", kk["kamers"])
+ kamers = int(m.group(1)) if m else None
+
+ # Parse slaapkamers: "3" or "3 slaapkamers" → 3
+ slaapkamers = slaapkamers_card
+ if kk.get("slaapkamers"):
+ m = re.search(r"(\d+)", kk["slaapkamers"])
+ slaapkamers = int(m.group(1)) if m else slaapkamers_card
+
+ listings.append(RawListing(
+ url=detail_url,
+ source_makelaar="schielandborsboom",
+ status=status,
+ adres=adres,
+ postcode=kk.get("postcode"),
+ stad=stad,
+ prijs=prijs,
+ hero_image_url=hero,
+ woningtype=kk.get("woningtype"),
+ bouwjaar=int(kk["bouwjaar"]) if kk.get("bouwjaar") else None,
+ woonoppervlak=parse_m2(kk.get("woonoppervlak")) or woonoppervlak_card,
+ perceeloppervlak=parse_m2(kk.get("perceeloppervlak")),
+ kamers=kamers,
+ slaapkamers=slaapkamers,
+ energielabel=kk.get("energielabel") or energielabel_card,
+ ))
+ if config.APP_ENV == "dev":
+ break
+ except Exception as e:
+ log.warning("schielandborsboom: parse fout: %s", e)
+
+ if len(cards) < 18:
+ break
+ page += 1
+
+ log.info("schielandborsboom: %d listings opgehaald", len(listings))
+ return listings
+
+
# ---------------------------------------------------------------------------
# SCRAPERS — exporteer hier alle actieve SSR adapters
# ---------------------------------------------------------------------------
@@ -882,4 +1050,5 @@ SCRAPERS = {
'dens': fetch_dens,
'3dmakelaars': fetch_3dmakelaars,
'dupont': fetch_dupont,
+ 'schielandborsboom': fetch_schielandborsboom,
}
diff --git a/tests/test_adapters.py b/tests/test_adapters.py
index 00d242a..51ac910 100644
--- a/tests/test_adapters.py
+++ b/tests/test_adapters.py
@@ -16,7 +16,7 @@ logging.basicConfig(
)
# --- change this to test a different adapter ---
-ADAPTER = SCRAPERS['dupont']
+ADAPTER = SCRAPERS['schielandborsboom']
if __name__ == "__main__":
print(f"Testing adapter: {ADAPTER.__name__}")