# realtycheck recorded-sale history → master catalogue

**Status (2026-09-25):** code on `dev-wk`; crosswalk final (every HOLD re-checked on 2026-09-24, every
high-rise NONE with a catalogue candidate re-checked on 2026-09-25); both packages rebuilt on wk as
`realtycheck-package-20260925b-*` (high-rise only, and all segments). **Imported to the master on 2026-09-27** (`…-20260925b-all`, sync run #12: 5,657 created, 0 failed, 84,098 monthly rows, 29,976 sales, 0 skipped; provider id 9 on master and wk). Licensing of the realtycheck data for all
deployments was approved by the owner's boss on 2026-09-23. Target: *1.1 Subsale Highrise* — show recorded transactions beside EdgeProp's
asking-sale and rental figures; only matches verified at **≥90% confidence** are linked
automatically.

## What it does

Attaches realtycheck's recorded sale history to **existing** Malaysian catalogue projects:

- a **monthly series** per project — median sale price, median reported PSF, number of sales —
  which is what a project page draws as its transaction chart;
- the **recent individual sales** realtycheck displays per scheme (at most eight: month, floor
  area, price, PSF), for a "recent transactions" list and price-position analysis.

It never creates, deletes, re-resolves or publishes a catalogue project, and never changes a
canonical fact: a project's name, EdgeProp's `price_median` / `psf_median`, coordinates, segments,
publication and everything else stay with the providers and people that own them.

## How it works

### 1. Which realtycheck scheme is which catalogue project — the crosswalk

The research module's `propertylab_*` tables (a realtycheck.my archive, 18,917 Malaysian schemes,
snapshot collected 2026-09-07 — see [main/property-research](/docs/modules_handbook/main/property-research/readMe.md))
share no id with the catalogue, so every scheme was matched to a catalogue project and given one
of four decisions:

| Decision | Meaning | Reaches the catalogue? |
|---|---|---|
| **LINK** (auto, ≥90%) | The scheme IS this catalogue project | **Yes** — a source row plus its sale history |
| **HOLD** (same, <90%) | Probably the same project, evidence short of 90% — **none left**: on 2026-09-24 all 277 were researched again (realtycheck page, portal listings, EdgeProp's recorded sales) and each became LINK, RELATED or NONE. A future HOLD is handled the same way: set `decision` to `LINK` once verified, export again, import | No |
| **RELATED** (part of) | A phase / block of the project, or the flats of a mixed estate / township row | No — attaching its prices would describe the wrong thing |
| **NONE** | Not in the catalogue, a same-named place elsewhere, or a different property class / phase | No — stays only in `propertylab_*` |

Result (final, 2026-09-25): **5,657 LINK** (2,200 high-rise = phase 1; 3,378 landed + 79 commercial =
phase 2), 0 HOLD, 657 RELATED, 12,603 NONE. Two research rounds after the first matching:

- **HOLD re-check (2026-09-24):** 277 held schemes → 126 LINK, 57 RELATED, 94 NONE.
- **High-rise NONE re-check (2026-09-25):** the first round had missed real matches — realtycheck's
  pin is often a name geocode kilometres from the building, and spellings differ ("Greenlane" /
  "Green Lane", "Oren" / Orange, "Mozek" / Mosaic). Of 911 high-rise NONE schemes, 574 had a
  catalogue candidate (a shared distinctive name word in the same state, or any high-rise within
  1.2 km) and were researched on the web → 293 LINK, 65 RELATED, 216 NONE; the 337 with no candidate
  stay NONE. Klang Valley first: its 242 became 179 LINK / 39 RELATED / 24 NONE. The same research
  moved 4 earlier links to the right row (two source-less duplicates swapped for the EdgeProp row;
  PANGSAPURI CANTIK PERMAI moved to Marminton Homes) and re-segmented one mis-typed semi-D scheme
  to landed.

In both rounds a LINK still needed ≥90%, and Claude read every verdict before it was applied.

How the decisions were reached: name normalisation (JPPH abbreviations, land-title numbers,
building-type words, phase / section numbers) + a distance allowance by property type + property
class + median price and high-rise PSF → two rounds of AI review with a blind audit (100/100 rule
matches confirmed) → an independent second AI review of 1,761 doubtful cases → Claude adjudicated
every high-rise disagreement (241; ten earlier "confident" matches were found wrong and removed) →
landed / commercial disagreements linked only when both reviews agree AND the median price is
within ±15% → an estate-level rule: a high-rise scheme is never linked to a mixed estate /
township row unless that row's own median (and PSF) match it.

⚠️ **The crosswalk exists only on wk** (`storage/app/propertylab-catalogue-match/FINAL-crosswalk.csv`,
gitignored). The `propertylab_*` archive it was built from moved to the MASTER on 2026-09-25
([site-data-on-master.md](/docs/modules_handbook/shared/project-catalogue/site-data-on-master.md));
until wk runs `catalogue:push-site-data --set=property_research --apply`, the archive too exists
only on wk (`petav3wk`). The Hub never reads the crosswalk — it receives a package. The same folder
holds `DISCUSSION-LOG.md` — every question, decision and number from the owner discussion, plus the
plan for HOLD / RELATED / NONE — and the matcher scripts under `scripts/`.

### 2. Data model (all catalogue-family, owned by the master)

| Where | What |
|---|---|
| `data_providers` | One row, code `realtycheck` (`DataProvider::CODE_REALTYCHECK`). Its **id must equal the master's** — `data_providers` is read over the DEFAULT connection, sources live on the catalogue connection. The seed migration copies the master's id when it runs locally. |
| `catalog_project_sources` | One row per LINK scheme on the existing project: `external_id` = realtycheck scheme id, `source_project_name`, `source_url`, `fields.realtycheck` = {category, state, district, mukim, median_price, median_psf, sales_count, match{decision, segment, confidence, decided_by}}. **No canonical key** — see *Traps*. |
| `catalog_project_price_months` | One row per (source, month): `median_price`, `median_psf`, `transactions`. Read with `CatalogProject::priceMonths()`. |
| `catalog_project_transactions` | One row per (source, `source_key`): `month`, `area_sqft`, `area_basis`, `price`, `psf`. Read with `CatalogProject::saleTransactions()`. |

Why two new tables and not the existing ones: `catalog_unit_valuations` is keyed building → floor
→ unit, and realtycheck's rows carry no floor or unit number; a JSON column on `catalog_projects`
would be resolved by the merge and keep ONE source's series, while 316 of the 5,252 linked projects
(78 of the 2,119 high-rise ones) are fed by two or more realtycheck schemes.

### 3. Pipeline

1. **Export (wk, read-only)** — `php artisan propertylab:export-catalogue-package
   --crosswalk=storage/app/propertylab-catalogue-match/FINAL-crosswalk.csv --segment=all`
   (or `high-rise` / `landed` / `commercial`)
   writes `records.ndjson` (one line per LINK scheme: identity, target project **uuid** — integer
   ids differ between databases — monthly rows, individual sales) and `manifest.json` (counts,
   sha256, selection). Two packages were built (folder dates are Malaysia time):

   | Package (`storage/app/…`) | Schemes | Projects | Monthly rows | Sales | `complete` |
   |---|---|---|---|---|---|
   | `realtycheck-package-20260925b-high-rise` | 2,200 | 2,119 | 34,799 | 12,746 | false |
   | `realtycheck-package-20260925b-all` | 5,657 (2,200 high-rise · 3,378 landed · 79 commercial) | 5,252 | 84,098 | 29,976 | true |

   A read-only dry run of the adapter against the live master (2026-09-25) resolved every line of
   both — no unknown uuid, every target a Malaysian project. The earlier packages (2026-09-23/24, and
   `…-20260925-all` / `-high-rise` built before the NONE re-check) are in
   `storage/app/propertylab-catalogue-match/superseded/packages/` — do not import them.
2. **Ingest (Hub)** — `php artisan catalogue:sync realtycheck --file=<package dir>`.
   `RealtycheckAdapter` verifies the checksum, resolves each uuid to the project on this catalogue
   and **skips** (and counts, `skipped_unknown_project` in the run stats) any uuid it cannot find.
   The ingestion service writes the source row, then upserts the month and transaction rows.
3. **Distribution** — the master's new tables reach sites like every catalogue table: LIVE-mode
   sites read the master; COPY-mode sites need the catalogue migration (it creates the empty local
   table) and then `catalogue:mirror-from-master`, which discovers tables by name.

Behaviour that is deliberate:

- **Attach-only, twice — and never a delete.** The adapter never yields a record without a project
  id, and the provider is registered `attach_only` in `config/project_catalogue.php`, so the
  ingestion service fails any record that names no existing project instead of letting
  `upsertSource()` mint a canonical. The same flag stops a `--full` run from deleting a project
  whose realtycheck source it deactivates: realtycheck never created it, and a source-less legacy
  row would otherwise be removed by the orphan rule.
- **No re-resolve, no AI refresh, no scrape date.** `canonical_facts => false`: the ingestion service
  does not `resolve()` the projects a run touches. That is not an optimisation — `resolve()` would
  re-derive floor plans, re-check completeness and **unpublish** a published row that falls short,
  and on a project with no other active source it would empty `market_segments` and
  `name_translations` (a set union over sources that carry none). The same flag keeps a
  realtycheck source's `data_scraped_at` out of the project's own when a later provider resolves it.
- **An existing link wins.** `upsertSource()` keeps a source on the project it already belongs to,
  so a later package cannot MOVE a scheme to another project. Re-target one through Review &
  combine / `attachSource()`; drop one by leaving it out of a complete package run with `--full`.
- **Reruns upsert and never delete.** Month rows upsert on (source, month) and transactions on
  (source, source_key); a later package that no longer lists an old month or sale keeps it. Rerun
  the same package safely; a newer package updates values and adds newer sales.
- **Full snapshot only for a complete package.** `--full` deactivates the provider's sources absent
  from the package. The adapter honours it only when the manifest says `selection.complete`
  (an `--segment=all` export), so a phase-1 package can never deactivate phase-2 sources.
- **Sale history follows its source.** `ProjectCatalogueMergeService::attachSource()` (Review &
  combine) moves the source's month and transaction rows with it; ingestion repoints any stragglers.
- **Sale history blocks a delete**, like unit valuation history: the admin delete
  (`CatalogProjectRepository::delete()`) refuses with *recorded sale history*, and orphan cleanup
  (`deleteIfSafelyOrphaned()`) keeps the project even after every source is deactivated. A duplicate
  that carries it is **combined** into its twin (which moves the history), never deleted.

## Where it shows (2026-09-27)

**The project page's Transactions tab** (`ProjectDetailContent.vue` → [`SaleHistoryTab.vue`](/resources/js/Components/ProjectDetail/SaleHistoryTab.vue)),
right after Overview (after 360° Aerial when the project has one), on every surface that mounts the page body: the public `/{country}/projects/{slug}`,
the catalogue record's Live preview, a Sales Project's Property Preview, the Area Guide drawer and
Property Explorer's drawer. It shows the monthly median price or PSF (one line per linked scheme — a
toggle, never a second y-axis), recorded sales per month (a separate bar chart on the same month
axis), a table view, and the recent individual sales with a far-off PSF flagged *Unusual* (a bulk
deal, transfer or shop lot is still a recorded sale; nothing is dropped).

- **It opens on median PSF for high-rise and on median price for landed.** A high-rise project's
  monthly price swings with which units happened to sell (a studio month, then a 3-bed month); PSF
  does not. Landed PSF is on a different area basis (Traps), so landed opens on price and says so.
- **A month whose PSF is more than 2× (or under ½) the median of the other months is left OUT of the
  line** — one RM 3m bulk deal otherwise flattens five years of RM 700 psf into the axis floor. It
  stays in the table view, flagged, and a line under the chart counts what was left out
  (`unusualPoints()` in [`saleHistory.js`](/resources/js/utils/saleHistory.js)); the Latest-month tile
  carries the same *Unusual* flag instead of posing that sale as the price. The same 2× rule flags
  individual sales. Fewer than 3 other points = no judgement.
- **On a phone the recent-sales table drops its Size column** and prints the size under the price, so
  the PSF column is never pushed off-screen; the scheme name under the month is truncated (full name
  on hover).

- **Whether the tab exists** is `detail.sale_history` in the page payload (`CatalogueDetailService`):
  `{available, sources}` when the project has an ACTIVE realtycheck source and the viewer may see it,
  else null. Deciding by recorded sales, not by `market_segments` / `sale_status`, is deliberate: of
  the 5,252 linked projects 4,875 carry no segment and 3,217 no sale status.
- **The rows** load lazily from `GET /{country}/projects/{slug}/sale-history`
  (`ProjectDetailController::saleHistory` → [`CatalogueSaleHistoryService`](/app/Services/Property/CatalogueSaleHistoryService.php)),
  members only (guests 403, and see the tab locked like the page's other member sections via
  `locked.sales`), published-and-listed only for non-managers (`previewableProject`).
- **The service applies the Traps below**: active sources only; one series per scheme (max 8 — the
  validated palette; more are named in the sales list); a single-sale month or a sale recorded under
  two schemes counted once (same month + price + PSF, + area for sales); `thin` under 5 sales; `landed`
  when a scheme is Landed (the tab then says to compare by price).
- **Temporary lock — it follows `PROPERTY_EXPLORER_LOCKED`** (owner, 2026-09-28; it had its own
  `SALE_HISTORY_LOCKED` for one day, now gone). `features.property_explorer_locked`, default locked,
  fails closed; one check, [`SaleHistoryAccess`](/src/Analysis/Support/SaleHistoryAccess.php), used by
  the payload AND the endpoint. While locked only admins see the tab; an admin adds `?preview=locked`
  to a project page to see the member view. Set it to `false` / `0` (then `config:cache`) to open
  the Explorer and this tab together — the Explorer opens projects straight onto it.
- **Property Explorer reads the catalogue** (owner's ruling 2026-09-27: catalogue projects only). Its
  list and Heat map are the projects with recorded sale history
  ([`CatalogueExplorerIndex`](/src/PropertyLab/Research/CatalogueExplorerIndex.php)), a project opens as
  its project page in a drawer on the Transactions tab. Each Research tab has its own lock since
  2026-09-28 — `PROPERTY_EXPLORER_LOCKED`, `HEAT_MAP_LOCKED`, `AI_ADVISOR_LOCKED` (admins always). See
  [property-research](/docs/modules_handbook/main/property-research/readMe.md).
- **Members only see published projects** (owner's ruling 2026-09-27). Only 3 of the 5,252 linked
  projects are published today (66 of 19,507 MY rows), so after unlocking, a member's Explorer is
  near-empty until projects are reviewed and published; admins see every row, unpublished ones tagged.

## Runbook — importing a package on the Hub

1. Merge `dev-wk` → `master` and deploy the Hub.
2. On the Hub, **straight after that deploy**:
   `php artisan migrate --path=database/migrations/catalogue --database=catalogue` (master: the two
   tables + the provider row), then `php artisan migrate --path=database/migrations/catalogue` (the
   Hub's local copy — it copies the master's provider id). ⚠️ Until the master has the two tables,
   Review & combine, the admin delete and a `--full` sync's cleanup FAIL on the Hub — all three
   check for sale history before they delete. Site deploys never run the first line
   ([databases-and-distribution.md](/docs/modules_handbook/shared/project-catalogue/databases-and-distribution.md) §6.1).
3. Copy ONE package directory from wk to the Hub — `…-20260925b-all` puts everything in (landed and
   commercial are "backend first", so their data belongs in the master now even though no page
   shows it yet); `…-20260925b-high-rise` stages the high-rise ones first. Importing the high-rise
   package and later the all package is safe: the second run updates the 2,200 and adds the rest.
   ⚠️ An existing source link wins — a package cannot move a scheme. If a superseded package was
   ever imported somewhere, the 4 links the NONE re-check moved (DESA TUN RAZAK, both D'SECRET
   GARDEN schemes, PANGSAPURI CANTIK PERMAI — `path` contains "correction 2026-09-25" in the
   crosswalk) stay on their old rows there and must be moved by hand.
4. `php artisan catalogue:sync realtycheck --file=/path/to/<package dir>` — **without `--full`** on a
   first import. `--full` is refused for the high-rise package anyway; with an all package it
   deactivates every realtycheck source missing from it, so use it only with a package built from
   the latest crosswalk, when a revised crosswalk has DROPPED links.
5. Check the run: `catalog_sync_runs` newest `realtycheck` row is `success`; on a first import
   `stats.created` = the package's schemes (5,657 or 2,200; `updated` on a rerun), `stats.failed` = 0,
   `stats.price_months` / `stats.transactions` = the package's monthly rows / sales (neither package
   has a zero-price sale to skip) and `stats.skipped_unknown_project` = 0 — a non-zero count means projects were merged or deleted on
   the master since the crosswalk was built. `stats.failed` > 0 usually means a project's
   `country_id` is not Malaysia (`upsertSource()` refuses the pairing); read `stats.errors`.
   ⚠️ Deploy the Hub **before** any other site runs `php artisan migrate` with this code, so the
   master's provider id exists first. Both sides are at `AUTO_INCREMENT` 9 today, so the ids would
   match anyway, but only the master-first order guarantees it.
6. Production: check whether it runs COPY or LIVE (`CATALOGUE_USE_DEFAULT_CONNECTION` in its `.env` —
   see [databases-and-distribution.md](/docs/modules_handbook/shared/project-catalogue/databases-and-distribution.md) §2).
   COPY: deploy (creates the local tables), then mirror. *(2026-09-27: production is **LIVE** —
   `catalogue:mirror-from-master` says so and refuses to copy — so the import reached it with no
   step on its side.)*

## Traps

- **Never put a canonical key in the source `fields`.** `project_name` in particular: the merge
  resolves canonical fields from every active source, so a project without an EdgeProp source would
  be renamed to realtycheck's upper-case JPPH name. The source's display name travels as
  `source_project_name`, a SOURCE_ONLY key.
- **Landed PSF is not comparable across sources.** For condos / apartments / flats realtycheck's PSF
  matches EdgeProp's almost exactly; for landed homes it runs on a different area basis (median
  ratio ≈1.34). Compare landed homes by median PRICE only. Individual sales carry
  `area_basis = unknown`.
- **Individual sales are displayed rows**, at most eight per scheme and the most recent ones — not a
  full history, and not proof of a unique transaction. Label them as recent sales.
- **Thin series mislead.** Some linked schemes have one or two sales, and a few of those are not
  market prices (a PPR tenant purchase at RM35k; a RM120k part-share transfer at Symphony Court).
  The chart needs a minimum-sales rule or a visible "few sales" caution.
- **Read sale history through ACTIVE sources.** `priceMonths()` / `saleTransactions()` return every
  row, including those of a source a later complete package deactivated — i.e. a match that was
  revoked. A reader that ignores `catalog_project_sources.is_active` shows a revoked match's prices
  on the project. (Same connection on both sides, so `whereIn` on the active source ids is safe.)
- **One project can have several series.** 316 linked projects (78 high-rise) are matched by two
  or more schemes (a taman sold in parts, a condo recorded under two names, JPPH splitting one
  complex into condo + serviced-apartment schemes). Each series keeps its source; a chart either
  draws one line per source or combines months — never picks one silently.
- **Two series can hold the same sale.** JPPH sometimes records one transaction under both scheme
  names: across those 316 projects, 5 single-sale months match another series exactly (same month,
  price and PSF — e.g. KENANGA POINT / MENARA KENANGA, Jan 2024, RM430k). Combining series must
  de-duplicate on month + price + PSF. A tiny bucket scheme whose only sales repeat a linked
  scheme's is kept RELATED for this reason ("SS 16 SUBANG JAYA" → Saujana Residency).
- **wk's realtycheck snapshot is not complete.** Some schemes on the live realtycheck site are missing
  from `propertylab_*` (e.g. PANGSAPURI BAYU PUTERI-PJU 3, RESIDENSI SUTERA 7, P/PURI SERI JASA - SG
  BESI INDAH), so they are in no package — a later re-scrape would add them.
- **HOLD, RELATED and NONE are not in the catalogue.** A surface that lists realtycheck projects from
  the catalogue will not show them (most NONE schemes are small-town tamans, kampung, shops and
  industrial the catalogue never held).

## Related files

- [`app/Console/Commands/PropertyLab/ExportCataloguePackage.php`](/app/Console/Commands/PropertyLab/ExportCataloguePackage.php) — the export (wk)
- [`app/Services/Property/Ingestion/Adapters/RealtycheckAdapter.php`](/app/Services/Property/Ingestion/Adapters/RealtycheckAdapter.php) — the package reader
- [`app/Services/Property/Ingestion/CatalogueIngestionService.php`](/app/Services/Property/Ingestion/CatalogueIngestionService.php) — `attach_only`, `canonical_facts`, `syncPriceMonths()`, `syncTransactions()`
- [`app/Services/Property/ProjectCatalogueMergeService.php`](/app/Services/Property/ProjectCatalogueMergeService.php) — `source_project_name`, `moveSourceSaleHistory()`, `hasSaleHistory()`
- [`src/Analysis/Repositories/CatalogProjectRepository.php`](/src/Analysis/Repositories/CatalogProjectRepository.php) — `delete()`'s *recorded sale history* blocker
- [`src/Analysis/Reference/CatalogProjectPriceMonth.php`](/src/Analysis/Reference/CatalogProjectPriceMonth.php), [`CatalogProjectTransaction.php`](/src/Analysis/Reference/CatalogProjectTransaction.php)
- [`database/migrations/catalogue/2026_09_23_2000*`](/database/migrations/catalogue/) — the two tables and the provider row
- [`config/project_catalogue.php`](/config/project_catalogue.php) — `providers.realtycheck`
- [`tests/Feature/Property/RealtycheckIngestionTest.php`](/tests/Feature/Property/RealtycheckIngestionTest.php)
- [`app/Services/Property/CatalogueSaleHistoryService.php`](/app/Services/Property/CatalogueSaleHistoryService.php) — the read side: `summary()` for the page payload, `history()` for the tab
- [`src/Analysis/Support/SaleHistoryAccess.php`](/src/Analysis/Support/SaleHistoryAccess.php) — the tab's gate (follows `PROPERTY_EXPLORER_LOCKED`)
- [`resources/js/Components/ProjectDetail/SaleHistoryTab.vue`](/resources/js/Components/ProjectDetail/SaleHistoryTab.vue), [`resources/js/utils/saleHistory.js`](/resources/js/utils/saleHistory.js) — the tab and its chart configs (validated palette)
- [`src/PropertyLab/Research/CatalogueExplorerIndex.php`](/src/PropertyLab/Research/CatalogueExplorerIndex.php) — Property Explorer's catalogue list
- [`tests/Feature/Property/CatalogueSaleHistoryTest.php`](/tests/Feature/Property/CatalogueSaleHistoryTest.php), [`tests/Feature/Portal/PropertyLabAdvisorTest.php`](/tests/Feature/Portal/PropertyLabAdvisorTest.php)

## Open work

- ~~Import on the Hub~~ done 2026-09-27. Production: if it runs COPY, deploy then `catalogue:mirror-from-master --apply`.
- ~~The transaction chart and recent-sales list~~ and ~~the Explorer switch~~ done 2026-09-27 (see
  *Where it shows*), behind ONE lock since 2026-09-28: `PROPERTY_EXPLORER_LOCKED=false` opens the
  Explorer and the Transactions tab together (wk opened both on 2026-09-28). Decide the source
  attribution wording shown to members (the tab says "recorded sale transactions", naming no provider).
- The mobile app's project endpoints return the same `detail` (so `sale_history` too) but have no
  Transactions view and no `/sale-history` mirror yet.
- The AI Advisor still reads the raw realtycheck archive (scheme ids, saved conversations). It has
  its own `AI_ADVISOR_LOCKED` now, so it CAN open to members — but it will then show schemes the
  catalogue does not hold and ignore publication; moving it to catalogue projects fixes both.
- Publishing: members see sale history only on published projects — 3 of 5,252 linked today.
- Decide what RELATED may show (e.g. estate-level data, or a new catalogue project for the missing
  block). Candidates found during the re-checks but outside their scope: VILLA HEIGHT 2 → catalogue
  "Taman Villa Heights 2" (median RM350k vs RM337.5k, not yet verified to 90%); the landed TAMAN
  CANTIK PERMAI (7 sales, RM1.3M) = Marminton Homes' 8 terraces.
- Catalogue cleanup on the Hub: the NONE re-check listed about 400 catalogue rows that sit in
  duplicate groups or carry another building's numbers or address (e.g. Seri Nilam Penang with Ampang's address and a
  "Johor" state; Pangsapuri Cantik Butterworth pooling a neighbour's sales and a KL namesake's
  source) — wk-only list in `storage/app/propertylab-catalogue-match/review/_none_round_catalogue_issues.md`.
- Coverage: outside the Klang Valley, Penang and Johor Bahru the catalogue holds almost no completed
  buildings (Melaka and Perak have no EdgeProp resale rows, Pahang 1, Negeri Sembilan 4, Sabah 2
  rows), so most of the 216 researched NONE are real buildings with no project to attach to.
