How We Built an AI-Native Real Estate Platform for Northern Cyprus Evlek, an AI-native property platform for Northern Cyprus, was built with a strict separation between language understanding and data truth to prevent AI hallucinations in real estate. The system uses a language model only for intent extraction, converting natural-language queries into structured search objects, while all property data comes from deterministic database retrieval and validation. While building Evlek, an AI-native property platform for Northern Cyprus, we encountered a problem that appears in almost every AI search product: A language model can understand what the user wants, but it should not be allowed to invent the facts used in the answer. This becomes especially important in real estate. A fabricated product recommendation may be inconvenient. A fabricated property price, title deed type, availability status, or location can influence a high-value decision. We therefore designed our property-search architecture around a strict separation: The model never becomes the source of truth. This article explains why we chose that architecture, how a natural-language property query becomes a structured search, and how we preserve uncertainty when the underlying data is incomplete. Most property platforms are built around filters: Filters are effective when users already understand the market and know exactly what they want. Many users do not begin with a filter configuration. They begin with a sentence: Find me a modern two-bedroom apartment in Kyrenia below £180,000, close to the sea and suitable for investment. This request contains both explicit and implicit information. The explicit constraints include: The softer preferences include: A language model can interpret those signals well. The danger begins when it is also expected to provide the actual listings from memory or from an unverified text context. A fluent answer is not necessarily a factual answer. We use the model for language understanding, not property truth. The simplified request flow looks like this: text Natural-language request │ ▼ Intent extraction │ ▼ Schema validation │ ▼ Database retrieval │ ▼ Deterministic filtering │ ▼ Relevance ranking │ ▼ Grounded explanation Every property presented to the user must originate from a real record returned by the application. The language model may explain why that property is relevant, but it cannot create a new listing, alter its price, add an unconfirmed amenity, or infer a legal field that is missing. Converting language into structured intent The first step is converting an open-ended sentence into a predictable search object. A simplified TypeScript representation might look like this: type PropertySearchIntent = { transaction?: "sale" | "rent" | "daily"; propertyTypes?: Array< "apartment" | "villa" | "house" | "land" | "commercial" ; city?: string; districts?: string ; bedrooms?: number; minimumPriceGbp?: number; maximumPriceGbp?: number; features?: string ; preferences?: { nearSea?: boolean; newBuild?: boolean; investmentFocused?: boolean; furnished?: boolean; }; }; The model is asked to populate this structure rather than produce a list of properties. For the example request, the result could resemble: { "transaction": "sale", "propertyTypes": "apartment" , "city": "Kyrenia", "bedrooms": 2, "maximumPriceGbp": 180000, "features": , "preferences": { "nearSea": true, "newBuild": true, "investmentFocused": true } } This object is not trusted automatically. It still passes through application validation. Validation must happen outside the model A model can return malformed, contradictory, or unsupported values. Examples include: A negative maximum price A city that does not exist in the platform Unsupported transaction types Bedroom values represented as text A district assigned to the wrong city A currency that the search service does not support The application must normalise and validate these fields before any query is executed. A simplified example: function validateSearchIntent input: PropertySearchIntent : PropertySearchIntent { const output: PropertySearchIntent = {}; if input.transaction === "sale" || input.transaction === "rent" || input.transaction === "daily" { output.transaction = input.transaction; } if typeof input.maximumPriceGbp === "number" && input.maximumPriceGbp 0 { output.maximumPriceGbp = input.maximumPriceGbp; } if typeof input.bedrooms === "number" && Number.isInteger input.bedrooms && input.bedrooms 0 { output.bedrooms = input.bedrooms; } return output; } The real implementation also needs controlled vocabularies, location resolution, aliases, multilingual values, and relationships between regions. The important principle is that the application—not the language model—decides which values are valid. Hard constraints and soft preferences are different Not every part of a user request should become a strict database filter. Consider: I want a sea-view apartment in Kyrenia below £180,000. The budget may be a hard constraint. The sea view may be a strong preference. If we require every result to contain a confirmed sea view = true field, the search may return nothing—even when relevant listings mention sea proximity in another structured attribute. We therefore divide the request into different signal types. Hard constraints These normally exclude a listing when they are not satisfied: Transaction type Maximum price Required bedroom count Explicit property type Required city or district Strong preferences These influence ranking but may not exclude every result: Sea view New development Furnished Walking distance to amenities Investment potential Contextual preferences These may require further interpretation: Good for a family Suitable for students Quiet area Strong rental potential Modern design This distinction gives the system room to return useful alternatives without pretending that every result is an exact match. Retrieval comes before explanation After validation, the application queries the property database. The retrieval layer should return structured records such as: type PropertyRecord = { id: string; title: string; transaction: "sale" | "rent" | "daily"; propertyType: string; city: string; district?: string; bedrooms?: number; priceGbp?: number; features: string ; titleDeedType?: string; publicationStatus: "active" | "inactive"; publishedAt: string; updatedAt: string; }; The language model does not create or modify these records. A result can only be shown if: It exists in the database It is available to the current user Its publication status permits display It satisfies the validated hard constraints It passes any additional business rules Only after this retrieval step does the model receive the approved result set. Preventing fabricated attributes Suppose a listing contains: { "bedrooms": 2, "priceGbp": 175000, "features": "shared pool", "balcony" , "titleDeedType": null } The model must not transform this into: This two-bedroom apartment includes a shared pool, balcony, and Turkish title deed. The title deed field is unknown. The grounded explanation should instead say: This listing matches the requested bedroom count and budget. It includes a shared pool and balcony. The title deed type is not specified in the available listing data. This is less polished than inventing a complete answer. It is also the correct answer. We found that explicit handling of missing information is one of the most important features of trustworthy AI search. Representing uncertainty in the response Missing data and negative data are not the same. These statements have different meanings: The property does not have a pool. The listing does not mention a pool. Pool information has not been confirmed. A useful property model should distinguish among them. For example: type AttributeStatus