Searches real estate in Turkey.
city, listingType, and mainCategory are required for listing searches — the user must specify which city to search in, whether they are looking for a property for sale or for rent, and the main category of the property.
**Listing type (`listingType`) — important:**
`listingType` accepts **only** `Satılık` or `Kiralık`. **Günlük Kiralık (daily / short-term rental) is NOT supported** — do not invent or send it as a listingType value.
When the user asks for günlük kiralık, kısa süreli kiralama, tatil için günlük, or similar:
1. Tell the user that daily rental search is not supported on this platform.
2. Do **not** call the tool with a made-up listingType.
3. Optionally offer a monthly `Kiralık` search instead — and make clear results are monthly rentals, not daily.
Do **not** confuse günlük kiralık with `residentialType: Yazlık` — Yazlık is a residential sub-type under monthly `Kiralık`, not daily rental.
**Firm / agency search (`firmQuery`) — important:**
When the user asks for a real-estate agency, emlak ofisi, gayrimenkul firması, or a named office chain (e.g. "Remax", "Kandemir Emlak", "Century 21 ofisleri"):
1. Set `firmQuery` to the firm name (do NOT use `poiQuery` for the firm name).
2. Send required `city`, `listingType`, `mainCategory` — `listingType` / `mainCategory` filter the firm's listings; `city` is ignored for firm name matching (schema default is fine).
3. When the user named a place, put intentional city and/or county into `locationQuery` (e.g. ["Ankara"], ["Üsküdar"], ["İstanbul", "Üsküdar"]). The server filters firms by location parsed from suggestion names (`(İl, İlçe)`).
4. After matching firms, the server fetches listings by `firmId`: **1 firm → up to 20 listings**; **multiple firms → up to 20 listings total**, split across firms. Response includes both `properties` and `firmSuggestions`.
5. If many offices match and the user wants one office only, ask for city/county and call again with `firmQuery` + `locationQuery`.
**No exact firm match — names only (critical):**
When the response has `structuredContent.exactFirmMatch === false` (or the message mentions "No exact firm match" / "NAMES only"):
- Tell the user in Turkish that no exact match was found for `requestedFirmQuery`.
- Present `firmSuggestions` as **names only** — e.g. "Aradığınız **[isim]** adında birebir bir ofis bulamadım. Benzer ofis isimleri bunlar, haberin olsun:" — list each with city/county from the tool text. Do not invent extra offices.
- If the user says "listele", "list them", "göster", or similar: **repeat those names**. Do **not** call this tool again.
- Do **not** offer to scan / tarama / ofisleri ayrı ayrı tara. That causes repeated listing+map requests and is forbidden.
- Do **not** call `search_real_estate` once per office. Never loop over `firmSuggestions`.
- Call the tool again **only** after the user picks **one** office (set `firmQuery` to that office's core name) or a city/county (`locationQuery`).
- Do **not** present alternative names as confirmed matches or fetch their listings unless the user picked one.
| User phrase | firmQuery | locationQuery |
|-------------|-----------|---------------|
| "Kandemir Emlak" / "Remax ofisleri" | "Kandemir Emlak" / "Remax" | omit |
| "Ankara'daki Kandemir Emlak" | "Kandemir Emlak" | ["Ankara"] |
| "Üsküdar Remax" | "Remax" | ["Üsküdar"] |
| "İstanbul Üsküdar Kandemir" | "Kandemir" | ["İstanbul", "Üsküdar"] |
Do **not** set `firmQuery` for normal listing searches (district/area/price/rooms). Do **not** put a firm name in `locationQuery`.
Location hierarchy (from largest to smallest):
- city (İl): Top level, e.g., İstanbul, Ankara (required for listing search)
- county (İlçe): Under city, e.g., Şişli is a county in İstanbul
- district (Mahalle): Under county, e.g., Fulya is a district in Şişli
- area: Specific neighborhoods or landmarks within a county
Use the locationQuery parameter to narrow down to one or more counties, districts, or areas within the city (union search). Send as an array — e.g. ["Kadıköy"] for a single location, ["Kadıköy", "Beşiktaş"] for multiple. If no locationQuery is provided, searches the entire city. For firm search, locationQuery is an optional city/county filter on firm suggestions (schema city is ignored).
Fill `poiQuery` and/or `locationQuery` from intent (see POI section): only POI → radius only; only location → district only; both → independent searches unioned. For multi-district radius-only POI pins (e.g. "Beylikdüzü, Esenyurt metrobüse yakın"), use `poiQuery` array without `locationQuery`.
**Multi-location (locationQuery array) — intent mapping:**
| User phrase | locationQuery |
|-------------|---------------|
| "Kadıköy'de ara" | ["Kadıköy"] |
| "Kadıköy ve Beşiktaş'ta" | ["Kadıköy", "Beşiktaş"] |
| "Moda, Caddebostan ve Fenerbahçe" | ["Moda", "Caddebostan", "Fenerbahçe"] |
| "Kadıköy veya Üsküdar'da olsun" | ["Kadıköy", "Üsküdar"] |
| "İstanbul Kadıköy Moda'da" | ["İstanbul / Kadıköy / Moda"] |
| "Kadıköy'ün Moda ve Caddebostan mahalleleri" | ["Kadıköy / Moda", "Kadıköy / Caddebostan"] |
Rules:
- `city` is always a single province; all `locationQuery` elements must be within that city.
- Multiple elements are combined with OR (union) — listings matching any location are returned.
- Use slash notation for disambiguation: "county / district" (e.g. "Kadıköy / Moda").
- Do NOT join multiple locations into one comma-separated string — use separate array elements.
**Point of Interest (POI) search:**
When the user references a landmark, park, hospital, venue, address, or "X yakınında / çevresinde" (e.g. "Gülhane Parkı yakınındaki ilanlar", "Galata Kulesi çevresinde", "İstinyePark AVM yanı"):
1. Set required `city` to the city where the place is located (e.g. İstanbul).
2. Do NOT set `latitude` or `longitude` — the server resolves coordinates via Google Geocoding.
3. Fill `poiQuery` and/or `locationQuery` from **user intent** (decision table below). The server runs a search for each field that is set: both → two independent searches unioned (deduped); only poi → radius only; only location → district/area only.
**Intent decision — when to fill which field:**
| Intent signal | Fill |
|---------------|------|
| Only "X yakınında / etrafında / çevresinde" (radius only) | `poiQuery` only — leave `locationQuery` empty/omitted |
| Only district/area listing search ("Y'de", "Y satılık", "Y'de ara") | `locationQuery` only — leave `poiQuery` empty/omitted |
| BOTH district coverage AND POI radius (e.g. "Bahçelievlerde **veya** Bahçelievler metrobüse yakın", "Beşyol metrobüs etrafında **ve** Mecidiyeköy") | Fill **both** independently from what the user asked |
Critical rules:
- Do **not** auto-copy districts out of `poiQuery` into `locationQuery` unless the user also asked for that district as a listing area (signals like **veya**, **ve**, "X'te" + "X metrobüs", etc.).
- When both are set, the server runs both searches and unions results (deduped by listing id).
- `poiQuery` still embeds the district in the POI string for geocoding (`"Bahçelievler metrobüs"`); that does **not** replace `locationQuery`.
| User phrase | poiQuery | locationQuery |
|-------------|----------|---------------|
| "Bahçelievlerde veya Bahçelievler metrobüse yakın satılık 2+1" | "Bahçelievler metrobüs" | ["Bahçelievler"] |
| "Beşyol metrobüs etrafında ve Mecidiyeköy satılık" | "Beşyol metrobüs" | ["Mecidiyeköy"] |
| "Bahçelievler metrobüse yakın" (radius only) | "Bahçelievler metrobüs" | omit |
| "Kadıköy'de satılık" | omit | ["Kadıköy"] |
| "Galata Kulesi yakını veya Kadıköy'de" | "Galata Kulesi" | ["Kadıköy"] |
**POI format rule — embed district in poiQuery for geocoding:**
- String: `"<ilçe> <poi>"` → e.g. `"Bahçelievler metrobüs"`, `"Kadıköy metro"`
- Array element: `"<ilçe> <poi>, <city>"` → e.g. `"Bahçelievler metrobüs, İstanbul"`
- Named landmarks with no ambiguity may omit district: `"Galata Kulesi"`, `"Pamukkale Üniversitesi"`
**Multi-district POI-only (radius-only — no "veya ilçe" / full-district signal):**
When the user names multiple districts AND a shared POI and wants **radius pins only** (no separate full-district listing search):
- Build `poiQuery` as a **string array** — one entry per district, format: `"<ilçe> <poi>, <city>"`
- Do NOT set `locationQuery`
- Max 10 entries
| User phrase | poiQuery |
|-------------|----------|
| "Beylikdüzü, Esenyurt, Avcılar, Bahçelievler metrobüse yakın satılık daire" | ["Beylikdüzü metrobüs, İstanbul", "Esenyurt metrobüs, İstanbul", "Avcılar metrobüs, İstanbul", "Bahçelievler metrobüs, İstanbul"] |
| "Kadıköy ve Üsküdar'da metro yakını" | ["Kadıköy metro, İstanbul", "Üsküdar metro, İstanbul"] |
**External / pre-resolved addresses:**
If you already have geocode-ready strings (from another resolver or MCP client), pass them as `poiQuery` array. Format: `"<ilçe> <poi>, <city>"`. Also set `locationQuery` only when the caller intends a full district/area search in addition to POI radius.
**Room / Section count fields – important distinction:**
- **For residential properties (konut / daire / residence type)** — when `mainCategory` is residential:
Use `roomTypes` as a string array of Turkish "X+Y" (or studio) values. Multiple entries are OR-combined.
| User phrase | roomTypes |
|-------------|-----------|
| "3+1 daire" | ["3+1"] |
| "2+1 ve 3+1" / "2+1 veya 3+1" | ["2+1", "3+1"] |
| "1+1, 2+1 veya 3+1" | ["1+1", "2+1", "3+1"] |
| "en az 2+1, en fazla 3+1" / "2+1 ile 3+1 arası" | ["2+1", "3+1"] |
| "stüdyo" | ["Stüdyo"] |
| "stüdyo veya 1+1" | ["Stüdyo", "1+1"] |
Valid values: `"Stüdyo"`, `"1+1"`, `"2+1"`, `"3+1"`, `"4+1"`, `"4+2"`, `"5+1"`, etc.
**"1+0" is NOT valid — for studio apartments always use `"Stüdyo"`.**
→ Automatically detect "X+Y" notation in the user's query and map it to `roomTypes`.
- **For commercial properties (işyeri / office / shop / warehouse etc.)** — when `mainCategory` is commercial:
- `sectionNumber`: number of sections / rooms / compartments (bölüm sayısı)
Commonly used in Turkish commercial listings as "3 bölümlü", "4 odalı ofis", etc.
Example mapping: "3 bölümlü" or "3 odalı" → sectionNumber: 3
**Summary – which field to use depending on mainCategory:**
| mainCategory | Use these fields for room/section count | Turkish example notation |
|----------------|-----------------------------------------|--------------------------|
| Residential | roomTypes (string array, OR) | 3+1, 2+1, 4+2, stüdyo |
| Commercial | sectionNumber | 3 bölümlü, 4 odalı |
Recognize and convert this popular notation automatically when the user uses it in their query.
**Building age (buildingAge) — mapping guidelines:**
buildingAge accepts an array, so multiple age ranges can be sent together to widen the search.
| User intent / phrase | buildingAge value |
|------------------------------------------------------------------|--------------------------------|
| "sıfır daire", "sıfır ev", "hiç kullanılmamış" | ["New Build"] |
| "yeni daire", "yeni yapı", "yeni bina", "az yaşlı bina" | ["New Build", "1-5"] |
| "1-5 yaş", "5 yaşından küçük", "5 yaşında" | ["1-5"] |
| "6-10 yaş", "10 yaşında", "10 yıllık bina" | ["6-10"] |
| "11-15 yaş", "15 yaşında" | ["11-15"] |
| "16-20 yaş", "20 yaşında" | ["16-20"] |
| "21 yaş ve üzeri", "21+ yaş", "21 yaşından büyük", "21 yıllık+" | ["21+"] |
| "depreme dayanıklı", "sağlam bina", "deprem yönetmeliğine uygun" | ["New Build", "1-5", "6-10"] |
- "Sıfır" (zero / brand-new, never occupied) → only **New Build**.
- "Yeni daire" or similar phrases implying a relatively new but not necessarily brand-new property → **New Build** + **1-5**.
- Earthquake-safety related phrases ("depreme dayanıklı", "sağlam bina") → **New Build** + **1-5** + **6-10** (all buildings constructed under modern seismic codes).
- Explicit age ranges ("X yaş", "X yaşında", "X yaş ve üzeri", "X yıllık bina") → map to the matching `buildingAge` bucket(s). **Never** use `floorNumbers` for these.
**Critical disambiguation — "yaş" (building age) vs "kat" (floor):**
- "X yaş" / "X yaşında" / "X yaş ve üzeri" / "X yıllık bina" → ALWAYS `buildingAge`. NEVER `floorNumbers`.
- Example: "21 yaş ve üzeri" → `buildingAge: ["21+"]` — do **not** set `floorNumbers: ["21 ve üzeri"]`.
- "X. kat" / "X kat" / "X ve üzeri kat" / "X. kat ve üzeri" → `floorNumbers` / `floorGroups`.
- If the user says "yaş" (age), do NOT map any number to `floorNumbers`.
**Floor (floorNumbers / floorGroups) — mapping guidelines:**
Floor filters apply **only** when `mainCategory` is Konut **and** `residentialType` is Daire. When the user mentions a floor preference, always set `residentialType: Daire`.
| User intent / phrase | Parameter |
|----------------------|-----------|
| "2. kat", "15. katta" | `floorNumbers: ["2"]`, `floorNumbers: ["15"]` |
| "1. veya 6. kat" | `floorNumbers: ["1","6"]` |
| "21 ve üzeri kat" / "21. kat ve üzeri" | `floorNumbers: ["21 ve üzeri"]` |
| "düz ayak", "basamaksız", "merdiven olmasın" | `floorGroups: ["duz_ayak"]` |
| "zemin kat", "giriş", "bahçe katı", "bodrum" | `floorGroups: ["alt_katlar"]` |
| "ara kat", "orta kat" | `floorGroups: ["orta_katlar"]` |
| "yüksek katta olsun" | `floorGroups: ["yuksek_katlar"]` |
| "en üst kat", "çatı katı", "teras" | `floorGroups: ["en_ust"]` |
| "zemin istemiyorum", "yükseklerde olsun" | `floorGroups: ["orta_katlar","yuksek_katlar","en_ust"]` |
| "çok yüksek olmasın" | `floorGroups: ["duz_ayak","orta_katlar"]` |
| "2. kat veya yüksek olsun" | `floorNumbers: ["2"]` + `floorGroups: ["yuksek_katlar"]` |
Critical rules:
- Exact floor number ("2. kat") → `floorNumbers: ["2"]` — **never** `floorGroups: ["orta_katlar"]` (that would return floors 2–10, not just 2).
- Named/fuzzy floor ("zemin kat", "yüksek") → `floorGroups`, not `floorNumbers`.
- `floorNumbers` and `floorGroups` can be combined (union of matched floors).
- Negative intent ("zemin olmasın") → select the remaining groups, not a separate exclude parameter.
- "21 yaş ve üzeri" is **not** a floor — use `buildingAge: ["21+"]`. Only use `floorNumbers: ["21 ve üzeri"]` when the word "kat" (or clear floor context) is present.
The response may include alternative location suggestions if multiple locations match the query.
search_real_estate