01Engine & dialect
SQLite 3.53.4 (better-sqlite3), opened read-only. Standard SQLite SQL: CTEs, window functions, json_each()/json_extract(), FTS5 MATCH/bm25(), R*Tree. Math functions (sqrt, pow, sin, cos, radians, …) are available; median(x), percentile(x, p) and percentile_cont(x, f) aggregates exist in this build (not in stock SQLite, so keep queries portable when you can); there is no REGEXP. Only one read-only statement (SELECT / WITH / EXPLAIN / VALUES) per request; use POST for long SQL (GET URLs over ~16 KB are rejected with HTTP 431); PRAGMA statements are blocked — introspect with SELECT * FROM pragma_table_info('deals') (also pragma_index_list, pragma_index_info, pragma_table_list) or sqlite_schema.
02Types, units and NULLs
Money is INTEGER shekels (₪, ILS). Areas are m². Price per m² (ppsqm) is ₪/m². Distances are metres (straight line). Booleans are INTEGER 0/1. *_pct columns are percent points (3.8 = +3.8%). NULL means unknown or not applicable — never 0.
03Dates and periods
Dates are TEXT 'YYYY-MM-DD' and compare correctly as strings (deal_date >= '2025-07-01'). Months are 'YYYY-MM', quarters 'YYYY-Qn' (sort correctly). Deals cover 1998-01-01 … 2026-09-17. Use substr(deal_date, 1, 7) for the month.
04Hebrew text
Place names are Hebrew (UTF-8, logical order) in the raw registry spelling: ASCII " and ' are used as gershayim/geresh ('ניר ח"ן', 'ג'ת'), e.g. 'תל אביב-יפו', 'ירושלים', 'חיפה', 'באר שבע', 'קריית שמונה', 'הרצלייה' (note: many names use קריית, some קרית; Herzliya is spelled הרצלייה). Match exact names with =, or search with the FTS5 tables. settlements.name_en has English names. English → code shortcuts: Tel Aviv-Yafo 5000, Jerusalem 3000, Haifa 4000, Rishon LeZion 8300, Petah Tikva 7900, Ashdod 70, Netanya 7400, Be'er Sheva 9000, Holon 6600, Bnei Brak 6100, Ramat Gan 8600, Rehovot 8400, Herzliya 6400, Kfar Saba 6900, Ra'anana 8700, Modi'in 1200, Eilat 2600.
05Identifiers and join keys
settlement_code = CBS locality code (settlements.code). A property is located by (gush, chelka, sub_chelka): gush = cadastral block, chelka = parcel, sub_chelka = unit (0 = none). (gush, chelka) joins deals ↔ parcels and every per-parcel enrichment table. nbhd_id joins neighbourhoods, streets.id joins streets. parcels.id is a build-local rowid used only for parcels_rtree.
06The stat rule (in_stats)
Residential price statistics use only deals.in_stats = 1: residential group, not an outlier, not multi-unit, not a discount lottery project, a full deal (portion = 1) and deal_amount ≥ ₪100k (₪50k before 2005). ₪/m² statistics additionally need price_per_sqm IS NOT NULL. Counts (deals, deals_12m, …) include EVERY row. For non-residential groups in_stats is 0; apply is_outlier = 0 AND is_multi_unit = 0 AND is_full_deal = 1 AND deal_amount >= floor (10k; commercial 50k) yourself.
07ppsqm = price per square metre
price_per_sqm (deals) and median_ppsqm / median_ppsqm_* (summaries) are ₪ per m² of the unit's registered area, residential full deals only. They are the standard comparison metric between places. Hide a median when its n (n_ppsqm*) is < 5 and flag 'few deals' when < 20.
08Time windows end at the stats anchor
meta.stats_anchor_date = 2026-06-30 (the last complete month). Window columns: 12m = 2025-07-01…2026-06-30, prev12m = 2024-07-01…2025-06-30, 24m = 2024-07-01…2026-06-30, prev24m = 2022-07-01…2024-06-30, 5y = 2021-07-01…2026-06-30. Never compute 'last 12 months' from today or from max_date; read bounds from meta: (SELECT value FROM meta WHERE key = 'window_12m_start').
09Incomplete months (is_incomplete)
Deals are reported with a lag, so months after the anchor (from 2026-07; quarter 2026-Q3; year 2026) are incomplete and geographically biased. Every agg_* row in that period has is_incomplete = 1: filter is_incomplete = 0 for trends, or label them partial. They are not a price or volume drop.
10Price changes are existing-stock changes
ppsqm_change_pct (settlements, districts, neighborhoods) and change_pct (gushim) compare existing-stock (is_new_build = 0) medians between windows and are NULL unless both windows have ≥ 50 (gushim: 20) such deals. Use them instead of dividing pooled medians, which swing with the new-build mix. Never average medians across cells or years.
11Partial deals, outliers, multi-unit rows
deal_amount is the price of the sold share: when is_full_deal = 0 (portion < 1) it is not comparable with full prices. is_outlier (with outlier_reason) and is_multi_unit (with multi_unit_kind) rows are kept for completeness but excluded from statistics; for 'most expensive / cheapest deal' lists add is_outlier = 0 AND is_multi_unit = 0 AND is_full_deal = 1.
12Discount lottery projects
is_discount_project = 1 marks units sold at a subsidised price in lottery programmes (מחיר למשתכן, מחיר מטרה, דירה בהנחה): real sales, not in statistics. discount_source = 'official' rows link to discount_projects via discount_project_id; 'heuristic' rows were found by price rules. settlements.n_discount_12m / gushim.n_discount_24m count them.
13Geo precision
Every deal of a (gush, chelka) shares one point (deals.lat/lon = parcels.lat/lon). geo_precision: 'parcel' (91%; parcels.geo_source 'parcel' = exact, 'cancelled'/'shuma' = approximate), 'gush' (8.8%, the gush centroid) or 'settlement' (0.02%, the town centre — not a location, excluded from parcels_rtree). Addresses come from OpenStreetMap (parcel_address): street_source = 'osm_nearest_street' is only a nearby street.
14Full-text search (FTS5) with Hebrew
settlements_fts (name, aliases; rowid = settlements.code), streets_fts (street, settlement, aliases; rowid = streets.id; contentless) and neighborhoods_fts (name, settlement, aliases; rowid = nbhd_id; contentless). Tokenizer unicode61 remove_diacritics 2 with prefix indexes. Syntax: WHERE settlements_fts MATCH '"באר"*' (quoted token + * = prefix), AND-ed tokens: '"רמת" "גן"'; column filter: 'street:"הרצל" AND settlement:"רחובות"'. Remove ASCII " and ' from user text first (ת"א → תא) — a bare quote is FTS5 syntax. Order with bm25(settlements_fts, 5.0, 1.0) (lower is better). Contentless tables return NULL columns: join the base table.
15Bounding boxes (R*Tree)
SELECT p.gush, p.chelka, p.lat, p.lon FROM parcels_rtree r JOIN parcels p ON p.id = r.id WHERE r.minLat >= :south AND r.maxLat <= :north AND r.minLon >= :west AND r.maxLon <= :east. For deals in a box join deals ON d.gush = p.gush AND d.chelka = p.chelka (or use the deals_rtree view). Keep boxes small (a few km) and add a date filter.
16Performance
Queries time out after 10 s and results are capped (default 1,000 rows, max 10,000). deals has 3.18M rows: always filter it by an indexed column and add LIMIT. Indexes on deals: ix_deals_amount(deal_amount); ix_deals_date(deal_date, id, settlement_code, …); ix_deals_discount_project(discount_project_id); ix_deals_group_amount(property_group, deal_amount); ix_deals_group_date(property_group, deal_date, id, …); ix_deals_gush_chelka(gush, chelka, deal_date); ix_deals_ppsqm(price_per_sqm, id, settlement_code, …); ix_deals_settlement_amount(settlement_code, deal_amount); ix_deals_settlement_date(settlement_code, deal_date, id, …); ix_deals_settlement_group_date(settlement_code, property_group, deal_date, …); ix_deals_settlement_ppsqm(settlement_code, price_per_sqm). Prefer agg_settlement_quarter/year, agg_national_*, agg_district_*, agg_gush_year, agg_neighborhood_year and the summary columns of settlements / gushim / parcels / neighborhoods / streets — they answer most statistics questions in < 1 ms. count(*) over all deals is fine (~30 ms); a GROUP BY over all deals without an indexed filter takes seconds. Check plans with EXPLAIN QUERY PLAN.
17Pseudo-groups and buckets in agg_* tables
property_group has the 9 real groups plus 'all_residential' (the 4 residential groups) and 'all' (every deal; medians NULL). rooms_bucket has '1-2', '3', '4', '5', '6+' and 'all' (totals incl. unknown rooms). Always pin both dimensions (e.g. property_group = 'all_residential' AND rooms_bucket = 'all') or sums will double count. Cells without deals are absent.
18Real (inflation-adjusted) prices
macro_month.real_factor converts a nominal ₪ amount of that month to June-2026 ₪: JOIN macro_month m ON m.month = substr(d.deal_date, 1, 7) and multiply. For yearly series use the year's mean factor.
19Attribution
Source: Israel Tax Authority real-estate transactions (public). Enrichment: OpenStreetMap contributors (ODbL), CBS, Bank of Israel, government ministries (see meta.sources). Data is provided as-is without warranty; it is not an appraisal.