# Polish Real Estate Aggregator (`trev0n/polish-real-estate-aggregator`) Actor

Unified real-estate search across Poland's 5 largest portals (Otodom, Morizon, Gratka, OLX, Nieruchomości-online). Runs all in parallel and groups duplicate listings - when the same property is advertised on multiple portals, you get one canonical row plus a list of all sources.

- **URL**: https://apify.com/trev0n/polish-real-estate-aggregator.md
- **Developed by:** [Paweł](https://apify.com/trev0n) (community)
- **Categories:** Real estate, Automation, Developer tools
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, NaN bookmarks
- **User rating**: No ratings yet

## Pricing

$9.99 / 1,000 results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## 🏠 Polish Real Estate Aggregator

🎯 **Search Poland's top 5 real estate portals at once — deduplicated, unified, one click.**

Automatically collect property listings from **Otodom, Morizon, Gratka, OLX and Nieruchomości-online** in a single run. The aggregator scrapes them in parallel, normalizes every field into one schema, and detects the same physical apartment advertised on multiple portals. You get a clean dataset where every listing has a `clusterId` grouping all its cross-portal copies.

### 🚀 What Does It Do?

This aggregator replaces five separate scrapers with **one single input and one unified output**. Set your search criteria once — city, price range, area, rooms — and it queries all five portals simultaneously, cleans the data, then deduplicates listings that appear on multiple platforms.

💡 **Three things happen automatically under the hood:**

1. **🌐 Parallel multi-portal search** — All five portals scraped at once with the same filters
2. **🧬 Smart cross-portal matching** — A multi-signal algorithm detects when the same property appears on different portals
3. **🏆 Canonical selection** — From each cluster, the algorithm picks the listing with the most complete data as the canonical record

### 👥 Who Is This For?

| 🏢 Use Case                 | 💬 How It Helps                                                                            |
| --------------------------- | ------------------------------------------------------------------------------------------ |
| 📊 **Real Estate Agencies** | See the full market at once instead of manually checking five sites separately             |
| 🔎 **Property Investors**   | Compare prices across portals for the same property — spot price differences and arbitrage |
| 📣 **Market Researchers**   | Accurate market-size statistics without double-counting cross-posted listings              |
| 🏗️ **Proptech Platforms**   | Clean, deduplicated inventory feed for aggregator sites or CRM systems                     |
| 🌍 **Relocation Services**  | Offer clients the widest selection of Polish properties from a single source               |
| 🧾 **Data Analysts**        | Measure market velocity, listing overlap rates, and price dispersion across portals        |
| 💼 **Mortgage & FinTech**   | Reliable property data pipeline without building integrations to each portal               |
| 🏘️ **Valuation Companies**  | Multi-source price comparables for appraisal reports                                       |

### ✨ Features

- 🌐 **5 Portals at Once** — Otodom, Morizon, Gratka, OLX, and Nieruchomości-online in a single run
- 🧬 **Smart Deduplication** — Cross-portal matching detects the same property listed on multiple sites
- 🏆 **Canonical Listings** — Algorithm picks the richest record from each cluster as the reference
- 🔗 **clusterId on Every Record** — Group all copies of the same property with one field
- 📊 **Multi-Source Visibility** — See exactly which portals list each property and at what price
- 🏘️ **All Property Types** — Apartments, houses, plots, and commercial properties
- 💰 **Sale & Rent** — Both transaction types supported
- 🗺️ **Any Polish City** — Kraków, Warszawa, Wrocław, Poznań, Gdańsk, Łódź, Katowice, Szczecin, and more
- 🎛️ **Smart Filters** — Price, area, rooms — applied consistently across all five portals
- 📸 **Full Photo URLs** — Direct links to listing photos from every source
- 📍 **GPS Coordinates** — Latitude and longitude for every listing (where available)
- 👤 **Seller Info** — Agency names and phone numbers when provided by the source
- 🎚️ **Tunable Confidence** — Adjust the dedup threshold to trade off precision for coverage
- ⚡ **Fast & Scalable** — Full 5-portal run in under a minute
- 📤 **Export Anywhere** — Download results as JSON, CSV, Excel, or push to Google Sheets, Zapier, Make, or your CRM

### 🧬 How Cross-Portal Matching Works

Same apartment posted on three portals? The aggregator detects that. Under the hood it runs a confidence-scored matching pipeline that combines multiple listing characteristics — when the combined score crosses your configured threshold, the records are merged into a single cluster.

You can tune the threshold in the input:

- **Higher threshold** → stricter matching, fewer merges, near-zero false positives
- **Lower threshold** → broader matching, catches harder cases at the cost of occasional false merges
- **Default 85** → calibrated sweet spot from internal benchmarks

### 🎛️ Filters & Options

| Option                            | What It Does                                                                           |
| --------------------------------- | -------------------------------------------------------------------------------------- |
| 🏙️ **City**                       | Any Polish city (e.g., `Kraków`, `Warszawa`, `Wrocław`)                                |
| 🏠 **Property Type**              | Apartment, House, Plot, or Commercial                                                  |
| 🏷️ **Search Type**                | Sale or Rent                                                                           |
| 💰 **Price Min / Max**            | Filter by price range in PLN                                                           |
| 📐 **Area Min / Max**             | Filter by area range in square meters                                                  |
| 🛏️ **Rooms Min / Max**            | Filter by number of rooms                                                              |
| 🌐 **Portals**                    | Choose which of the five portals to include (default: all)                             |
| 🎚️ **Match Confidence Threshold** | Confidence cutoff for auto-merging (50-100, default 85)                                |
| ⚡ **Fast Mode**                  | Faster runs at the cost of matching accuracy — recommended only for very large queries |
| 🔢 **Max Items Per Portal**       | Limit per-portal extraction — final dataset can be up to N × 5 listings before dedup   |

### 📦 What You Get (Output Fields)

Every record in the output dataset represents one listing on one portal, annotated with its cluster.

#### Cluster & Source

| Field       | Example                                            |
| ----------- | -------------------------------------------------- |
| clusterId   | `c00025`                                           |
| clusterSize | `3`                                                |
| isCanonical | `true`                                             |
| portal      | `otodom`                                           |
| offerId     | `ID4ANTy`                                          |
| url         | `https://www.otodom.pl/pl/oferta/2-pokoje-ID4ANTy` |
| scrapedAt   | `2026-04-24T15:48:19.324Z`                         |

#### Listing Basics

| Field         | Example                                       |
| ------------- | --------------------------------------------- |
| title         | `2 pokoje / jasna kuchnia / 45,8m² / piwnica` |
| price         | `698000`                                      |
| priceCurrency | `PLN`                                         |
| area          | `45.8`                                        |
| rooms         | `2`                                           |
| floor         | `4`                                           |
| totalFloors   | `6`                                           |
| buildYear     | `2012`                                        |
| marketType    | `wtorny`                                      |

#### Location

| Field     | Example         |
| --------- | --------------- |
| city      | `Kraków`        |
| district  | `Prądnik Biały` |
| street    | `Legnicka`      |
| latitude  | `50.085012`     |
| longitude | `19.923456`     |

#### Contact

| Field      | Example        |
| ---------- | -------------- |
| phone      | `+48124326750` |
| agency     | `Nova Estate`  |
| sellerType | `agency`       |

#### Media & Description

| Field           | Example                                                          |
| --------------- | ---------------------------------------------------------------- |
| imageUrls       | `["https://ireland.apollo.olxcdn.com/...", "..."]`               |
| descriptionText | `Mieszkanie 2-pokojowe w doskonałej lokalizacji, po remoncie...` |

#### Only on Canonical Records

| Field   | Example                                                                                                       |
| ------- | ------------------------------------------------------------------------------------------------------------- |
| portals | `["otodom", "morizon", "gratka"]`                                                                             |
| sources | `[{"portal": "otodom", "url": "...", "price": 698000}, {"portal": "morizon", "url": "...", "price": 698000}]` |

### 📊 Example Output

```json
{
    "clusterId": "c00025",
    "clusterSize": 3,
    "isCanonical": true,
    "portal": "otodom",
    "offerId": "ID4ANTy",
    "url": "https://www.otodom.pl/pl/oferta/2-pokoje-jasna-kuchnia-45-8m2-piwnica-parking-ID4ANTy",
    "title": "2 pokoje / jasna kuchnia / 45,8m² / piwnica / parking",
    "price": 698000,
    "priceCurrency": "PLN",
    "area": 45.8,
    "rooms": 2,
    "floor": 4,
    "totalFloors": 6,
    "buildYear": 2012,
    "marketType": "wtorny",
    "city": "Kraków",
    "district": "Prądnik Biały",
    "street": "Legnicka",
    "latitude": 50.085012,
    "longitude": 19.923456,
    "phone": "+48124326750",
    "agency": "Nova Estate",
    "sellerType": "agency",
    "imageUrls": [
        "https://ireland.apollo.olxcdn.com/v1/files/abc123/image;s=1080x720",
        "https://ireland.apollo.olxcdn.com/v1/files/def456/image;s=1080x720"
    ],
    "descriptionText": "Mieszkanie 2-pokojowe w doskonałej lokalizacji, po remoncie, z piwnicą i miejscem postojowym...",
    "dateCreated": "2026-04-10T12:34:56+02:00",
    "dateModified": "2026-04-22T09:12:00+02:00",
    "portals": ["otodom", "morizon", "gratka"],
    "sources": [
        { "portal": "otodom", "url": "https://www.otodom.pl/pl/oferta/...-ID4ANTy", "price": 698000 },
        { "portal": "morizon", "url": "https://www.morizon.pl/oferta/...-mzn2047091708", "price": 698000 },
        { "portal": "gratka", "url": "https://gratka.pl/nieruchomosci/...-46482277", "price": 698000 }
    ],
    "scrapedAt": "2026-04-24T15:48:19.324Z"
}
````

### 📋 Dataset Views

The Apify Console gives you **2 ready-made table views** to quickly browse your results:

| View                  | What It Shows                                                                     |
| --------------------- | --------------------------------------------------------------------------------- |
| 📊 **Overview**       | All listings grouped by `clusterId`, showing portal, title, price, area, and URL  |
| 🏆 **Canonical Only** | One row per unique property — canonical record with list of portals it appears on |

### ❓ FAQ

**🤔 Why use an aggregator instead of scraping each portal separately?**
Because the same apartment often appears on 2-3 portals, you'd get duplicate rows in your data. The aggregator detects those duplicates and groups them — you get accurate unique-listing counts and can compare prices across portals for the same property.

**🤔 What happens if the deduplication is too aggressive?**
Adjust the `scoreThreshold` — 85 is the default sweet spot (zero false positives in our tests). Raise it to 90 for maximum precision, or drop it to 70 if you'd rather catch more matches at the cost of occasional false merges.

**🤔 Does it handle the primary market (new developments)?**
Yes, but conservatively. When two listings at the same address have different prices, they're treated as different units in the same building (not merged). This prevents the "developer lists 200 identical apartments" edge case from merging into one cluster.

**🤔 Why do some portals show fewer details than others?**
Each portal exposes different fields. OLX and Nieruchomości-online hide phone numbers behind a click, so those records have `phone: null`. The matching algorithm compensates with the other available signals.

**🤔 Can I skip one of the portals?**
Yes — use the `portals` input parameter to select only the ones you want. Running with 3 portals instead of 5 is proportionally faster.

**🤔 Can I export the data?**
Yes — JSON, CSV, Excel, XML, HTML, RSS. You can also push data directly to Google Sheets, Zapier, Make, or any webhook/API endpoint.

**🤔 How often should I run this?**
For fresh data, run daily or weekly. You can schedule automatic runs on Apify with just a few clicks.

**🤔 Does it work with proxies?**
Yes — residential proxies are recommended for the best results on Otodom (which uses anti-bot protection). The aggregator works with Apify's built-in proxy service.

### 🛠️ Need Custom Filters or Features?

**I'm happy to customize this aggregator for your specific needs!** 🤝

Whether you need:

- 🎯 Additional filters (commute time, specific districts, building year, features like balcony/parking)
- 📊 Extra data fields or custom output formats (e.g., price-per-square-meter summary, weekly delta reports)
- 🔄 Integration with your CRM, Google Sheets, or database
- ⏰ Scheduled scraping with automatic deduplication history across runs
- 🌐 Adding more portals (Adresowo.pl, Domiporta.pl, Sprzedajemy.pl, GetHome.pl) to the aggregation
- 🧬 Tuning the matching algorithm for a specific market segment (primary market, luxury, commercial)

👉 **Don't hesitate to reach out via private message** — I respond quickly and I'm always open to building exactly what you need. No request is too small or too specific!

### ⚖️ Legal & Ethical Use

This aggregator collects **only publicly available listings** from Otodom, Morizon, Gratka, OLX and Nieruchomości-online. It does not access private data, bypass authentication, or extract contact details that are not publicly displayed. Please use the data responsibly and in compliance with applicable laws and each portal's terms of service.

# Actor input Schema

## `city` (type: `string`):

City to search (e.g., 'Kraków', 'Warszawa', 'Wrocław'). Used for all portals.

## `propertyType` (type: `string`):

Type of property.

## `searchType` (type: `string`):

Sale or rent.

## `priceMin` (type: `integer`):

Minimum price in PLN.

## `priceMax` (type: `integer`):

Maximum price in PLN.

## `areaMin` (type: `integer`):

Minimum area in square meters.

## `areaMax` (type: `integer`):

Maximum area in square meters.

## `roomsMin` (type: `integer`):

Minimum number of rooms.

## `roomsMax` (type: `integer`):

Maximum number of rooms.

## `maxItemsPerPortal` (type: `integer`):

Limit applied to each scraper separately. Final dataset can be up to N × (number of portals) before dedup.

## `portals` (type: `array`):

Select which portals to query. Defaults to all 5.

## `scoreThreshold` (type: `integer`):

Confidence cutoff (0-100) for merging two listings into the same cluster. Higher values are stricter (fewer merges, near-zero false positives); lower values catch more cross-portal matches at the cost of occasional false merges. 85 is the recommended default.

## `skipPhash` (type: `boolean`):

Run with reduced matching accuracy for faster execution. Recommended only for very large queries — leave off for best deduplication quality.

## `emitAllListings` (type: `boolean`):

By default the dataset contains one row per unique property (the canonical listing), with all cross-portal variants collapsed into the `sources` field. Enable this to also emit the per-portal duplicate listings (marked isCanonical=false) — useful for comparing prices or wording across portals, but creates multiple rows per property.

## `phashCacheName` (type: `string`):

Name of the named Apify key-value store used to cache matching data across runs. Any two runs using the same store share the cache — repeated runs on overlapping listings reuse previously computed match data and run faster. Leave default unless you want to isolate caches per search profile.

## `otodomProxy` (type: `object`):

Proxy used when scraping Otodom. Defaults to Apify Residential PL — Otodom's anti-bot (DataDome) blocks datacenter IPs, so this is required for it to work. Override only if you know what you're doing.

## `morizonProxy` (type: `object`):

Proxy used when scraping Morizon. Default: no proxy (Morizon doesn't require it).

## `gratkaProxy` (type: `object`):

Proxy used when scraping Gratka. Default: no proxy (Gratka doesn't require it).

## `olxProxy` (type: `object`):

Proxy used when scraping OLX. Default: no proxy (OLX doesn't require it).

## `nolProxy` (type: `object`):

Proxy used when scraping Nieruchomości-online. Default: no proxy (NoL doesn't require it).

## Actor input object example

```json
{
  "city": "Kraków",
  "propertyType": "mieszkanie",
  "searchType": "sprzedaz",
  "maxItemsPerPortal": 50,
  "portals": [
    "otodom",
    "morizon",
    "gratka",
    "olx",
    "nol"
  ],
  "scoreThreshold": 85,
  "skipPhash": false,
  "emitAllListings": false,
  "phashCacheName": "aggregator-phash-cache",
  "otodomProxy": {
    "useApifyProxy": true,
    "apifyProxyGroups": [
      "RESIDENTIAL"
    ],
    "apifyProxyCountry": "PL"
  }
}
```

# Actor output Schema

## `overview` (type: `string`):

No description

## `clusters` (type: `string`):

No description

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "city": "Kraków"
};

// Run the Actor and wait for it to finish
const run = await client.actor("trev0n/polish-real-estate-aggregator").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = { "city": "Kraków" }

# Run the Actor and wait for it to finish
run = client.actor("trev0n/polish-real-estate-aggregator").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "city": "Kraków"
}' |
apify call trev0n/polish-real-estate-aggregator --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=trev0n/polish-real-estate-aggregator",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Polish Real Estate Aggregator",
        "description": "Unified real-estate search across Poland's 5 largest portals (Otodom, Morizon, Gratka, OLX, Nieruchomości-online). Runs all in parallel and groups duplicate listings - when the same property is advertised on multiple portals, you get one canonical row plus a list of all sources.",
        "version": "1.0",
        "x-build-id": "8uzdt5ZV7Y6AbFxfH"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/trev0n~polish-real-estate-aggregator/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-trev0n-polish-real-estate-aggregator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/trev0n~polish-real-estate-aggregator/runs": {
            "post": {
                "operationId": "runs-sync-trev0n-polish-real-estate-aggregator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/trev0n~polish-real-estate-aggregator/run-sync": {
            "post": {
                "operationId": "run-sync-trev0n-polish-real-estate-aggregator",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "properties": {
                    "city": {
                        "title": "City",
                        "type": "string",
                        "description": "City to search (e.g., 'Kraków', 'Warszawa', 'Wrocław'). Used for all portals."
                    },
                    "propertyType": {
                        "title": "Property Type",
                        "enum": [
                            "mieszkanie",
                            "dom",
                            "dzialka",
                            "lokal"
                        ],
                        "type": "string",
                        "description": "Type of property.",
                        "default": "mieszkanie"
                    },
                    "searchType": {
                        "title": "Search Type",
                        "enum": [
                            "sprzedaz",
                            "wynajem"
                        ],
                        "type": "string",
                        "description": "Sale or rent.",
                        "default": "sprzedaz"
                    },
                    "priceMin": {
                        "title": "Price Min (PLN)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum price in PLN."
                    },
                    "priceMax": {
                        "title": "Price Max (PLN)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Maximum price in PLN."
                    },
                    "areaMin": {
                        "title": "Area Min (m²)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Minimum area in square meters."
                    },
                    "areaMax": {
                        "title": "Area Max (m²)",
                        "minimum": 0,
                        "type": "integer",
                        "description": "Maximum area in square meters."
                    },
                    "roomsMin": {
                        "title": "Rooms Min",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Minimum number of rooms."
                    },
                    "roomsMax": {
                        "title": "Rooms Max",
                        "minimum": 1,
                        "maximum": 10,
                        "type": "integer",
                        "description": "Maximum number of rooms."
                    },
                    "maxItemsPerPortal": {
                        "title": "Max Items Per Portal",
                        "minimum": 1,
                        "type": "integer",
                        "description": "Limit applied to each scraper separately. Final dataset can be up to N × (number of portals) before dedup.",
                        "default": 50
                    },
                    "portals": {
                        "title": "Portals to scrape",
                        "uniqueItems": true,
                        "type": "array",
                        "description": "Select which portals to query. Defaults to all 5.",
                        "items": {
                            "type": "string",
                            "enum": [
                                "otodom",
                                "morizon",
                                "gratka",
                                "olx",
                                "nol"
                            ],
                            "enumTitles": [
                                "Otodom",
                                "Morizon",
                                "Gratka",
                                "OLX",
                                "Nieruchomości-online"
                            ]
                        },
                        "default": [
                            "otodom",
                            "morizon",
                            "gratka",
                            "olx",
                            "nol"
                        ]
                    },
                    "scoreThreshold": {
                        "title": "Match Confidence Threshold",
                        "minimum": 50,
                        "maximum": 100,
                        "type": "integer",
                        "description": "Confidence cutoff (0-100) for merging two listings into the same cluster. Higher values are stricter (fewer merges, near-zero false positives); lower values catch more cross-portal matches at the cost of occasional false merges. 85 is the recommended default.",
                        "default": 85
                    },
                    "skipPhash": {
                        "title": "Fast Mode",
                        "type": "boolean",
                        "description": "Run with reduced matching accuracy for faster execution. Recommended only for very large queries — leave off for best deduplication quality.",
                        "default": false
                    },
                    "emitAllListings": {
                        "title": "Emit all per-portal listings",
                        "type": "boolean",
                        "description": "By default the dataset contains one row per unique property (the canonical listing), with all cross-portal variants collapsed into the `sources` field. Enable this to also emit the per-portal duplicate listings (marked isCanonical=false) — useful for comparing prices or wording across portals, but creates multiple rows per property.",
                        "default": false
                    },
                    "phashCacheName": {
                        "title": "Match cache store name",
                        "type": "string",
                        "description": "Name of the named Apify key-value store used to cache matching data across runs. Any two runs using the same store share the cache — repeated runs on overlapping listings reuse previously computed match data and run faster. Leave default unless you want to isolate caches per search profile.",
                        "default": "aggregator-phash-cache"
                    },
                    "otodomProxy": {
                        "title": "Otodom proxy",
                        "type": "object",
                        "description": "Proxy used when scraping Otodom. Defaults to Apify Residential PL — Otodom's anti-bot (DataDome) blocks datacenter IPs, so this is required for it to work. Override only if you know what you're doing.",
                        "default": {
                            "useApifyProxy": true,
                            "apifyProxyGroups": [
                                "RESIDENTIAL"
                            ],
                            "apifyProxyCountry": "PL"
                        }
                    },
                    "morizonProxy": {
                        "title": "Morizon proxy",
                        "type": "object",
                        "description": "Proxy used when scraping Morizon. Default: no proxy (Morizon doesn't require it)."
                    },
                    "gratkaProxy": {
                        "title": "Gratka proxy",
                        "type": "object",
                        "description": "Proxy used when scraping Gratka. Default: no proxy (Gratka doesn't require it)."
                    },
                    "olxProxy": {
                        "title": "OLX proxy",
                        "type": "object",
                        "description": "Proxy used when scraping OLX. Default: no proxy (OLX doesn't require it)."
                    },
                    "nolProxy": {
                        "title": "Nieruchomości-online proxy",
                        "type": "object",
                        "description": "Proxy used when scraping Nieruchomości-online. Default: no proxy (NoL doesn't require it)."
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
