# Data and method

ข้อมูลและวิธีของ CityMETER · Fitness รุ่น 0.3.1 (30 ตุลาคม 2569; ชีตวิจัยตรวจแหล่ง 26 ต.ค. 2569) ป้ายหลักฐานตาม Landometer Master Brand Brief v0.5.3 §3.7: **Observed fact** · **Inference** · **Hypothesis** · **Owner-stated** · **Project evidence** ตัวเลขทุกตัวในเอกสารนี้มาจากไฟล์ใน repository และอ้าง field path ได้ (`data/fitness.json` เว้นแต่ระบุไฟล์อื่น)

## 1. แหล่งข้อมูลและวันที่

| ชั้น | แหล่ง | วันที่ | ไฟล์ในเดโม |
|---|---|---|---|
| สาขา แบรนด์ บริษัท | ชีต “CityMETER Fitness Thailand — Research Master 2026-10-26” (ไฟล์ภายใน ไม่อยู่ใน repository; SHA-256 ของไฟล์ export ที่ใช้สร้างอยู่ใน `meta.exportSha256` = `4e10b8a1…600464`, export .xlsx วันที่ 30 ต.ค. 2569 หลังเขียนการผูกศูนย์การค้า (§7) — ค่าอื่นของทุกแถวตรงกับ export ของรุ่น 0.2.1 `2b1e1c38…a63d8` ต่างเฉพาะ `logo_status` แท็บโลโก้ที่เพิ่ม และคอลัมน์การผูกศูนย์การค้า) แท็บ `01_BRANDS` · `02_STORES` · `07_SHOPPING_CENTERS` · `11_COMPANIES` · `12_RELATIONSHIPS` · `14_NETWORK_COVERAGE` · `16_BRAND_LOGOS` | ตรวจแหล่ง 26 ต.ค. 2569 ทุกแถว (`cols.checked`) | `data/fitness.json` |
| หลักฐานรายสาขา | หน้าทางการของแต่ละแบรนด์: หน้าสาขา เว็บไซต์ หน้าค้นหาสาขา โซเชียลทางการ LINE OA หรือประกาศ — ลิงก์สาธารณะ https ต่อแถว (`urls[cols.url]`) | ตามวันตรวจของแถว | `cols.url`, `cols.source` (`vocab.sourceTypes`) |
| ศูนย์การค้า | ทะเบียน Shopping Center ของ CityMETER (แท็บ `07_SHOPPING_CENTERS` ของชีต: uuid ของ CityMETER, รหัสภายนอก, จุดอ้างอิง) · ศูนย์ที่แท็บนี้ไม่มีอ่านจาก snapshot สาธารณะของทะเบียน `montri-th/shoppingcenter@2eb5532` (asOf 26 ต.ค. 2569, hash ทุกไฟล์ที่อ่านรวมใน `meta.registry.files`) — 121 แห่ง | ตามชีต / ตาม commit | `venues[]` (`from`: `07` หรือ `registry`) |
| โลโก้แบรนด์ | แท็บ `16_BRAND_LOGOS` และคลังโลโก้ของทีมวิจัย (ไฟล์ตรวจ SHA-256 กับแท็บทุกไฟล์) | ตรวจ 29 ต.ค. 2569 | `data/brand-logos.json`, `assets/brand-logos/` |
| โลโก้ศูนย์การค้า | แท็บ `10_VENUE_LOGOS` ของทะเบียน Shopping Center (snapshot เดียวกัน) เฉพาะศูนย์ที่ผูกในชุดนี้ ไบต์เดิม | ตาม snapshot | `data/venue-logos.json`, `assets/venue-logos/` |
| ขอบเขตเขต/อำเภอ และจังหวัด | CityMETER production public API (`/base/info/lv0`, `/base/info/lv1/{province uuid}`: รหัส ชื่อไทย areaSqm) อ่านใหม่ 29 ต.ค. 2569 · เส้นขอบเขตจาก `montri-th/pharmacy@98d9035` ซึ่งดึง production SUBDISTRICT geometry เมื่อ 23 ต.ค. 2569 แล้วรวมเป็นเขต/อำเภอ (พิกัดไม่เปลี่ยน) | 23 และ 29 ต.ค. 2569 | `data/area-index.json`, `data/frames/{CODE}.json`, `data/provinces.geojson`, `data/boundary-receipts.json` |
| ชื่ออังกฤษของเขต/อำเภอ/ตำบล | OpenGISData-Thailand `districts.geojson` / `subdistricts.geojson` @ `fafcea1` (CC BY 4.0, อ้าง DOPA) — ใช้อ่านที่อยู่ภาษาอังกฤษเท่านั้น ไม่ใช้เป็นขอบเขต | ตาม commit | `area-index.json → metadata.englishNames` |
| แผนที่ฐานและมวลอาคารเมือง | OpenFreeMap (OpenMapTiles schema, © OpenStreetMap contributors) | โหลดตอนเปิดหน้า | ไม่มีไฟล์ใน repository |

**สถานะของชีต (Observed fact, `meta.sheetStatus = research_master`):** ชีตเป็นทะเบียนวิจัยที่เจ้าของสั่งให้รวบรวม (108 ชื่อที่เจ้าของยืนยัน + 17 ชื่อเดิมที่ตามตรวจเพิ่ม + 1 แบรนด์ในเครือที่มีหลักฐานในไทย = 126 แบรนด์, `brands[].origin`) ชีตเองเตือนว่าก่อนนำไปนับอุปทานต้องตรวจรูปแบบบริการ ตัวตนสาขา วันที่ข้อมูล และแยกแถวประกาศเปิดออก เดโมนี้แยกสถานะตามข้อนั้นและให้กรองรูปแบบได้ แต่ **ไม่ใช่การรับรองว่าข้อมูลพร้อมใช้เชิงพาณิชย์** และไม่ใช่สำมะโนฟิตเนสทั้งประเทศ

**การตรวจตัวเลขหัวตาราง (Observed fact, `meta.readmeCheck`):** สคริปต์สร้างข้อมูลหยุดทันทีถ้าตัวเลขที่ได้ไม่ตรงกับตัวเลขในแท็บ README ของชีตเอง — 126 แบรนด์ · 378 แถวใน `02_STORES` · 352 แถวที่ต้นทางยังแสดงบริการ · 28 แถวที่มีพิกัดจากแหล่ง · 44 บริษัท/กลุ่ม · 48 ความสัมพันธ์ และจำนวนแถวต่อแบรนด์ต้องตรงกับ `14_NETWORK_COVERAGE.observed_pos_rows` ทุกแบรนด์

## 2. หน่วยข้อมูลและหน่วยนับ

หนึ่งแถว = หนึ่งแถวของ `02_STORES` คือ **การพบสาขาหนึ่งในแหล่งทางการของแบรนด์** (`meta.unit.row`) ไม่ใช่ผลตรวจภาคสนาม เดโมไม่รวม แยก หรือย้ายแถวใด id ของแถวคือ id ของชีตเอง (`POS-FIT-…`)

| สถานะในเดโม (`vocab.status`) | แถว | สถานะในชีต (`statusRaw`) |
|---|---:|---|
| `listed` · เปิดตามแหล่ง | 352 | `source_listed` — หน้าทางการยังแสดงสาขา ณ วันตรวจ |
| `announced` · ประกาศเปิด | 19 | `announced_opening_soon` 14 · `announced_coming_soon` 2 · `announced_presale` 2 · `announced_opening_date_today` 1 |
| `closed` · ปิดแล้ว | 2 | `permanently_closed` 1 · `closed_official` 1 |
| `review` · ต้องตรวจสถานะ | 5 | `source_status_unknown` 2 · `primary_network_listed_location_unresolved` 1 · `official_profile_listed_source_conflict` 1 · `primary_listed_detail_refresh_pending` 1 |

**หน่วยนับ (สัญญาของผลิตภัณฑ์, `js/model.js#unitWords`, `filterRows`):**

- ค่าเริ่มต้นนับเฉพาะ `listed` เรียกว่า **“สาขา”** — สาขาที่หน้าทางการของแบรนด์ยังแสดง ณ วันตรวจ (ไม่ได้ยืนยันว่าเปิดประตูทุกวัน)
- สถานะอื่นเลือกได้จากตัวกรอง “สถานะ” และทุกคำเปลี่ยนเป็น **“รายการ”** ไม่บวกเข้ากับจำนวนสาขา หน้าแรกบอกจำนวนสถานะอื่นของตัวกรองเดียวกันไว้หนึ่งบรรทัด (“ไม่รวมในตัวเลข: ประกาศเปิด 19 · ปิดแล้ว 2 · ต้องตรวจสถานะ 5 รายการ”)
- `all` · ทุกสถานะ = 378 รายการ ใช้ดูภาพรวมเท่านั้น ไม่ใช่จำนวนสาขาที่เปิด
- ชุดข้อมูลครอบคลุม **126 แบรนด์ที่คัดไว้** (122 แบรนด์มีสาขาที่เปิดตามแหล่ง) ทุกตัวเลขจึงหมายถึง “สาขาของแบรนด์ในชุดข้อมูล” ไม่ใช่จำนวนฟิตเนสทั้งหมดในพื้นที่ ไม่ใช่ส่วนแบ่งตลาด ไม่ใช่ความจุหรือจำนวนสมาชิก

## 3. พื้นที่ของสาขา (เขต/อำเภอ)

หน่วยวิเคราะห์คือ **เขต/อำเภอของ CityMETER production** (รหัส DOPA 4 หลัก, 928 พื้นที่) เพราะแหล่งทางการของแบรนด์ส่วนใหญ่ให้ที่อยู่ ไม่ให้พิกัด และที่อยู่ระบุถึงเขต/อำเภอได้บ่อยกว่าถึงแขวง/ตำบล พื้นที่ของแถวถูกจับคู่ตอนสร้างข้อมูลเพียงครั้งเดียว (`cols.district`, `cols.admin`) แอปไม่ย้ายแถวจากจุดหรือชื่อร้านภายหลัง

| วิธี (`vocab.admin`) | ทุกแถว | เปิดตามแหล่ง | ความหมาย |
|---|---:|---:|---|
| `point` | 28 | 27 | จุดที่แหล่งเผยแพร่ตกในขอบเขตเขต/อำเภอ — จุดที่อยู่ใกล้ขอบไม่เกิน 150 ม. ตรวจซ้ำกับ production SUBDISTRICT geometry ความละเอียดเต็ม (`data/boundary-receipts.json → geometry`, 44 ใบ; 29 แถวรวมแถวที่ใช้จุดของศูนย์การค้า) |
| `host` | 129 | 121 | แถวที่ชีตผูกกับศูนย์การค้าในทะเบียน CityMETER ด้วย venue id → จุดอ้างอิงของศูนย์ → เขต/อำเภอ (ศูนย์ใน `07_SHOPPING_CENTERS`: สคริปต์ยืนยันว่าเขตตรงกับเขตที่ชีตระบุ · ศูนย์จาก snapshot ของทะเบียน: จุดเป็นตัวตัดสิน และรายงานเมื่อรหัสเขตในทะเบียนต่าง — Central Rama 9 ทะเบียนระบุ 1026 จุดตกห้วยขวาง 1017 · Lasalle’s Avenue ทะเบียนระบุ 1101 จุดตกบางนา 1047) |
| `address` | 142 | 134 | อ่านจากที่อยู่ของแถวเอง (`scripts/admin_text.py`) — `marker` 55 คำว่าเขต/อำเภอ/แขวง/ตำบล หรือ District/Khet/Amphoe/Tambon · `pair` 55 คำบริหารสองคำขึ้นไปที่ชี้เขตเดียวกัน · `trailing` 28 คำเขต/อำเภอคำเดียวที่ปิดท้ายที่อยู่ในจังหวัดที่ทราบ · `mueang` 4 คำว่า Mueang/เมือง ในจังหวัดที่ทราบ → อำเภอเมือง |
| `province` | 45 | 40 | ระบุได้แค่จังหวัด (จากชีต หรือจากจังหวัด/รหัสไปรษณีย์ที่อยู่ในจังหวัดเดียว) — นับในจังหวัดและยอดรวม ไม่อยู่ในสีของเขต/อำเภอใด |
| `conflict` | 1 | 1 | จังหวัดในชีตไม่ตรงกับที่อยู่ของแถวเอง (`meta.conflicts`: Jetts Rest Park Suvarnabhumi — ชีตระบุตราด ที่อยู่ชี้สมุทรปราการ) — ไม่วางในพื้นที่ใด รายงานไว้ ไม่เลือกข้างเอง |
| `none` | 33 | 29 | ไม่มีจังหวัดในแถว — นับในยอดรวมและบอกเป็น “ไม่ระบุจังหวัด” |

กติกาการอ่านที่อยู่ (Observed fact ในโค้ด, `scripts/admin_text.py`):

- ใช้ชื่อจากแหล่งเดียวกับรหัส: ชื่อไทยและรหัสจาก production base info · ชื่ออังกฤษจาก production และ OpenGISData (เฉพาะเมื่อรหัสและชื่อไทยตรงกัน) · ตัวสะกดอังกฤษเทียบด้วยโครงเสียง (เช่น th→t, ph→p, Sri→Si) ไม่ใช้ fuzzy match
- ทุกคำบริหารในที่อยู่ต้องสอดคล้องกับเขตเดียวกัน (คำแขวง/ตำบลต้องอยู่ในเขตนั้น) ถ้าขัดกันหรือกำกวม (ชื่อซ้ำหลายเขต เขตที่ยุบแล้ว) แถวนั้นหยุดที่จังหวัด
- คำบริหารคำเดียวที่ไม่ได้ปิดท้ายหรือไม่มีจังหวัดกำกับ ไม่ใช้จับคู่ (เช่น ชื่อสถานี BTS หรือชื่อย่านที่ซ้ำกับตำบลในจังหวัดอื่น)
- รหัสไปรษณีย์ใช้บอกจังหวัดเท่านั้น และเฉพาะรหัสที่อยู่ในจังหวัดเดียว ไม่ใช้บอกเขต/อำเภอ
- ชื่อร้านไม่ใช่ข้อมูลพื้นที่ — ไม่เคยใช้ชื่อสาขาหรือชื่อย่านในชื่อร้านจับคู่พื้นที่

**สรุปสาขาที่เปิดตามแหล่ง (Observed fact):** 282 จาก 352 สาขาระบุเขต/อำเภอได้ (76 เขต/อำเภอ, 23 จังหวัด) · 40 ระบุได้แค่จังหวัด · 29 ไม่ระบุจังหวัด · 1 จังหวัดขัดกัน · การผูกศูนย์การค้าในรุ่น 0.3.0 ทำให้ 34 แถวที่เดิมระบุได้แค่จังหวัดหรือไม่ระบุจังหวัดได้เขต/อำเภอ และ 87 แถวที่ระบุจากที่อยู่อยู่แล้วได้เขตเดิมทุกแถว ทุกหน้าที่จัดอันดับพื้นที่บอกจำนวนที่ไม่อยู่ในอันดับไว้ด้วย

## 4. พิกัด

- 157 แถวมีจุด (`points`): 28 จุดที่แหล่งเผยแพร่เอง (`store_source_point` · `operator_published_map_destination_point` · `official_social_profile_map_point_historical`) และ 129 แถวที่ใช้จุดอ้างอิงของศูนย์การค้าในทะเบียน CityMETER (`host_registry_reference_point` — จุดของศูนย์ ไม่ใช่ทางเข้าสาขา) · แถวที่มีจุดของตัวเองและผูกศูนย์การค้าด้วย (4 แถว) ใช้จุดของตัวเอง — Physique 57 Bangkok ที่ผูกกับ Erawan Bangkok มีจุดของแหล่งห่างจุดของศูนย์ 688 ม. (Observed fact; รายงานทีมวิจัยตรวจจุดของแหล่ง)
- แถวที่ไม่มีจุดไม่มีหมุดบนแผนที่ แต่ยังนับในเขต/อำเภอของตัวเอง ไม่มีการสร้างพิกัดจากที่อยู่หรือชื่อ (no geocoding)
- จุดไม่ได้ถูกปรับ ปัด หรือย้าย เว้นแต่การแยกหมุดที่ซ้อนกันบนจอ (ระยะบนจอเท่านั้น จุดจริงคงเดิม มีเส้นโยงกลับ)

## 5. ขอบเขตพื้นที่

- `data/area-index.json`: เขต/อำเภอ 928 และจังหวัด 77 จาก production base info — รหัส ชื่อไทย areaSqm ของ production จุดวางป้ายและ bbox (ไม่มี geometry) ใบรับ 78 ใบ digest `34abca05…77bd` (`data/boundary-receipts.json → baseSha256`, ตรงกับ `area-index.json → metadata.receiptsSha256` และ `fitness.json → meta.boundaries.receiptsSha256`)
- `data/frames/{CODE}.json`: เส้นขอบเขตเขต/อำเภอรายจังหวัด (77 ไฟล์, รวม 928) คัดจาก `montri-th/pharmacy@98d9035` ซึ่งดึง production SUBDISTRICT geometry 7,436 แขวง/ตำบล (ใบรับ 7,514 ใบใน repository นั้น) ลดจุดเพื่อแสดงผลแล้วรวมเป็นเขต/อำเภอ — ใช้แสดงผลและนำทางเท่านั้น ไม่ใช่ขอบเขตทางการ
- `data/provinces.geojson`: กรอบจังหวัดสำหรับมุมมองทั้งประเทศ คัดจากแหล่งเดียวกันโดยไม่แก้ไข
- แอปโหลดเส้นขอบเขตทีละจังหวัดตามที่ต้องใช้ (`js/frames.js`) ไม่โหลดทั้ง 77 ไฟล์ตั้งแต่เปิดหน้า

## 6. ตัวชี้วัดและการจัดระดับสี

- **จำนวน (LDS `count`)** ต่อเขต/อำเภอ ต่อจังหวัด และต่อช่องของตารางเมทริกซ์ — เป็นผลรวม ไม่มีตัวหาร **ไม่วาดความหนาแน่นต่อพื้นที่หรือต่อประชากร** เพราะชุดข้อมูลครอบคลุมเฉพาะแบรนด์ที่คัดไว้ อัตราส่วนต่อพื้นที่จะอ่านเหมือนมาตรวัดตลาดซึ่งไม่ใช่
- สีมาจาก LDS 0.9.7 `count` (scaleVersion `4ff5c1fb…0980`) LUT 41 ค่าเดียวกันทั้งสองธีม ตรวจด้วย `scripts/check-lds-parity.mjs` กับไฟล์ LDS ต้นฉบับ (`handoff/VERIFICATION.md`)
- **ใช้ครบ 41 ระดับ (กติกาประจำของเจ้าของ “Color scale ขอให้ใช้ 41 scales ตาม LDS เสมอ”, วิธีเดียวกับ CityMETER · Shopping centers, `js/scales.js#buildRamp`) ตั้งแต่รุ่น 0.2.0:** เรียงค่าที่วัดได้และมากกว่าศูนย์ของพื้นที่ที่กำลังอ่าน (เขต/อำเภอของจังหวัดที่เลือก หรือทุกเขต/อำเภอเมื่ออ่านทั้งประเทศ · กรอบจังหวัด · ช่องของเมทริกซ์) ค่าแต่ละค่าได้สีของ LUT ตามตำแหน่งกลางของลำดับ ปรับให้ค่าต่ำสุดอยู่ระดับ 0 และค่าสูงสุดอยู่ระดับ 40 (มีค่าเดียวอยู่ระดับ 40) ค่าเท่ากันได้สีเดียวกัน ไม่มีสีระหว่างระดับ ไม่มีช่วงเปิด แผนที่ คำอธิบาย ป้าย และกราฟจำนวนสาขาอ่าน ramp เดียวกัน คำอธิบายแผนที่วาดครบ 41 ระดับพร้อมตัวเลขอ้างอิง ณ ระดับของค่านั้น (รุ่น 0.1.0 ใช้ 5 ระดับเรขาคณิต 1–2–5 ซึ่งยกเลิกแล้ว)
- เหตุผลของการเรียงลำดับ: ค่าเป็นจำนวนเต็มเล็ก หางยาว (เขตส่วนใหญ่มี 1–4 สาขา มีไม่กี่เขตที่ 15–21) การเรียงลำดับทำให้ทุกระดับของสเกลถูกใช้ ข้อจำกัด: ระยะห่างของสีบอกลำดับ ไม่ใช่ผลต่างของจำนวน (16 กับ 21 ใกล้กัน 1 กับ 2 ห่างกัน) ตัวเลขบนป้ายและในกราฟจึงเป็นตัวอ่านหลัก
- ตัวเลขที่พิมพ์บนสีระดับ 27–29 (ไม่มีหมึกสีใดถึง 4.5:1) อยู่บนแผ่นรองสีกลาง สีข้อมูลไม่เปลี่ยน
- ศูนย์คือ **measured_zero** ไม่ใช่ระดับในสเกล (ดู §8)

### 6.1 รูปแบบ (format)

กลุ่มรูปแบบ 9 กลุ่มจัดจาก `segment` ของชีต (19 ค่า) ผูกกับสี series ของ LDS ด้วย id ที่คงที่ พร้อมรูปทรงประจำ series (DATAVIZ-05) ไม่ใช้ดาว (series 6) เพราะจะอ่านเป็นคะแนน

| กลุ่ม | segment ของชีต | series · รูปทรง | สาขาที่เปิดตามแหล่ง |
|---|---|---|---:|
| ยิม/ฟิตเนสคลับ | `gym_club` · `gym_chain` · `gym_independent` | 2 · square | 172 |
| ฟิตเนส EMS | `gym_chain_ems` | 3 · triangle | 24 |
| เทรนนิ่ง/PT | `strength_functional_pt` | 9 · dash | 36 |
| พิลาทิส/บาร์เร่ | `pilates_barre_lagree` · `pilates_reformer` · `pilates_hot_mat` · `pilates_multiclass` | 1 · circle | 31 |
| คลาสบูทีค/เต้น | `boutique_multiclass` · `dance_fitness` | 4 · diamond | 22 |
| โยคะ/แอเรียล | `yoga` · `yoga_aerial_pole` | 7 · hexagon | 10 |
| มวย/ต่อสู้ | `combat_fitness` | 5 · cross | 29 |
| ปีนผา/มูฟเมนต์ | `climbing_bouldering` · `parkour_movement` | 10 · plus | 23 |
| บริการเฉพาะ | `kids_activity` · `fitness_retreat_multisport` · `self_practice_rental` | 8 · ring | 5 |

**สตูดิโอ** (ใช้ในข้อค้นพบแรก) = เทรนนิ่ง/PT · พิลาทิส · คลาสบูทีค · โยคะ · มวย · ปีนผา **ยิม** = ยิม/ฟิตเนสคลับ + EMS · บริการเฉพาะไม่นับในทั้งสองฝั่ง (`js/model.js#FAMILY`)

## 7. แบรนด์ บริษัท และศูนย์การค้า

- **แบรนด์** 126 จาก `01_BRANDS` ผูกกับสาขาด้วย brand id ของชีต ไม่ใช้ชื่อ ความครอบคลุมรายชื่อสาขาของแต่ละแบรนด์ (`brands[].coverage`, จาก `branch_coverage`) แสดงเป็นภาษาไทยในบัตรแบรนด์: 59 แบรนด์ครบตามแหล่งทางการ · 63 บางส่วน · 4 กรณีเฉพาะ (สาขาเดียว ปิดแล้ว ฯลฯ)
- **บริษัท** 44 จาก `11_COMPANIES` และ **ความสัมพันธ์** 48 จาก `12_RELATIONSHIPS` ผูกด้วย id เท่านั้น — 45 ความสัมพันธ์บริษัท→แบรนด์ (บทบาท เช่น ผู้ดำเนินงานในไทย เจ้าของแบรนด์ ผู้ถือสิทธิ์ แพลตฟอร์มแฟรนไชส์) และ 3 ความสัมพันธ์บริษัท→บริษัท (ผู้ลงทุน) พร้อมลิงก์หลักฐานต่อความสัมพันธ์ 36 แบรนด์มีความสัมพันธ์ที่บันทึก
- ชื่อบริษัทเป็น **ชื่อตามที่แหล่งระบุ ยังไม่ได้ตรวจกับทะเบียนนิติบุคคล** (`companies[].verification = source_named_not_registry_certified` ทุกแถว) และ **การไม่พบบันทึกไม่ได้แปลว่าแบรนด์อิสระ** (กติกาของชีต) — หน้าจอพูดตามนี้
- “บริษัทเดียวกัน” ในบัตรแบรนด์ = แบรนด์อื่นในชุดข้อมูลที่บันทึกกับ company id เดียวกัน (เช่น Fitness First กับ Celebrity Fitness — Evolution Wellness) ไม่ใช้รายชื่อ sister brand ในแท็บ `15_SISTER_BRANDS` เพราะส่วนใหญ่ยังไม่พบหลักฐานว่ามีในไทย
- **ศูนย์การค้า** 128 แห่งจากทะเบียน Shopping Center ของ CityMETER (7 จาก `07_SHOPPING_CENTERS` · 121 จาก snapshot ของทะเบียน; id = uuid ของ CityMETER หรือรหัสทะเบียนเมื่อยังไม่มี uuid, `scId` = รหัสทะเบียน) ผูกกับ 147 แถวด้วย venue id ของชีต ไม่อนุมานจากชื่อหรือระยะ · 115 แห่งมีจุดอ้างอิง 13 แห่งทะเบียนยังไม่มีจุด (แถวของศูนย์เหล่านี้คงพื้นที่จากที่อยู่ หน้าศูนย์ใช้เขตที่แถวที่ผูกทั้งหมดตรงกัน)
- **จุดตกในอาคาร** (display-time, `js/building-join.js#joinBuildings`): จุดของแถวและตำแหน่งอ้างอิงของศูนย์การค้า × รูปอาคาร OpenStreetMap ที่แผนที่วาดในพื้นที่โฟกัส พูดว่า “จุดตกในอาคาร” เท่านั้น ไม่เปลี่ยนจำนวนใด และไม่ใช่การยืนยันว่าสาขาตั้งอยู่ในอาคาร ชั้น หรือห้อง · “อาคารเดียว” = ชิ้นของอาคารเดียวกันที่ถูกตัดที่ขอบ tile z14 (OSM id เดียวกัน และจุดตัดที่ขอบตรงกันภายใน 2 ม. — id อย่างเดียวไม่พอ เพราะ OpenFreeMap รวมอาคารสูงเท่ากันเป็น feature เดียว) · building:part ที่อยู่ในเส้นรอบอาคารที่ OpenMapTiles ซ่อนใน 3D (`hide_3d`) · ชิ้นที่วาดแยกแต่มีจุดเดียวกันอยู่ข้างใน (โพเดียมกับทาวเวอร์)
- **การผูกสาขากับศูนย์การค้า (สัญญา):** ใช้ `common_retail_venue_ids` (รหัสทะเบียน) และ `citymeter_shopping_center_id` (uuid) ของ `02_STORES` ที่มีสถานะ `accepted_identity_and_located_in` เท่านั้น สถานะที่ยังพักไว้ (`candidate_parent_component_review` · `candidate_registry_duplicate_review` · `candidate_name_or_numbering_review`) ไม่วาดเป็นการผูก · ศูนย์ที่มีจุดอ้างอิงใช้จุดนั้นแทนแถว (บทบาท `host_registry_reference_point` ไม่ใช่หน้าร้าน) ศูนย์ที่ไม่มีจุด แถวคงที่อยู่หรือจังหวัดของตัวเอง · ณ export นี้: ผูกแล้ว 147 แถว (มีจุด 133 · ศูนย์ไม่มีจุด 14) · พักไว้ 19 (ชื่อหรือเลขสาขากำกวม 8 · ทะเบียนซ้ำ 7 · ส่วนย่อยของศูนย์ 4) · ไม่ผูก 212 (`meta.venueLinks`)
- **การผูกรอบ 0.3.0 (เจ้าของ 29 ต.ค. 2569 “ผูกแล้วเขียนกลับเข้าชีตวิจัย”):** จับคู่แถวของ `02_STORES` กับศูนย์ในทะเบียนจากหลักฐาน — แหล่งทางการของสาขาระบุชื่อศูนย์ และเลขที่อยู่ตรงกับทะเบียน หรือชื่อนั้นมีรายการเดียวในทะเบียน (บางแถวตรวจเขตซ้ำ) วิธีบันทึกใน `host_join_method` (ลงท้าย `__review_20261029`) — ไม่ใช้ชื่อร้านหรือระยะ ผล: ผูกเพิ่ม 139 แถว (123 มี uuid ของ CityMETER · 125 มีจุดอ้างอิง) และพักไว้เพิ่ม 18 แถว รวม 908 เซลล์ใน 157 แถว พร้อมลิงก์หลักฐาน (หน้าทางการของสาขา + หน้าศูนย์ในทะเบียนตามรหัส) เขียนลงชีตวิจัยไฟล์เดิมเมื่อ 30 ต.ค. 2569 และอ่านกลับผ่าน export ทั้งไฟล์: ตรงแผนทุกเซลล์ ไม่มีเซลล์อื่นเปลี่ยน · เขตของแถวที่เคยระบุจากที่อยู่ไม่เปลี่ยนเลย
- **ตำแหน่งที่เจ้าของยืนยัน (0.3.1, Owner-stated):** เจ้าของข้อมูลให้ตำแหน่งบน Google Maps ของ Physique 57 Erawan (30 ต.ค. 2569 06:23) — 13.74408, 100.54077 ห่างจุดของ Erawan Bangkok (`SC-001278`) ในทะเบียน 56 ม. แต่ห่างจุดที่หน้าค้นหาสาขาทางการเผยแพร่ 739 ม. (จุดนั้นห่างจุดของศูนย์ 689 ม.) · บันทึกในชีตวิจัยไฟล์เดิมเป็นการตรวจแผนที่ของเจ้าของ: `02_STORES.map_audit_status` = `owner_confirmed_located_in_host_20261030` และต่อท้าย `notes` ของแถวนั้น (2 เซลล์ อ่านกลับผ่าน export ทั้งไฟล์: ตรงแผน ไม่มีเซลล์อื่นเปลี่ยน) · กติกาการสร้างข้อมูล: แถวที่มีค่านี้และผูกศูนย์ที่มีจุดแล้ว วาดที่จุดของศูนย์ (`host_registry_reference_point`) จุดของแหล่งยังอยู่ในชีตและยังนับใน `points.source` (28 ตาม README ของชีต) แต่ไม่วาด (`meta.ownerConfirmedHost`, `points.ownerConfirmedHost` = 1) · ค่าอื่นนอกจาก `not_field_audited` และรูปแบบนี้ทำให้การสร้างข้อมูลหยุด · แผงสาขาบอกในรายละเอียดแหล่งว่า “เจ้าของข้อมูลยืนยัน 30 ต.ค. 2569 ว่าสาขาอยู่ใน Erawan Bangkok · จุดที่หน้าทางการเผยแพร่ห่าง 689 ม. จึงไม่ใช้”
- **ลิงก์ข้ามไปทะเบียนศูนย์การค้า (0.3.1):** แผงศูนย์การค้าลิงก์ไปศูนย์เดียวกันใน CityMETER ศูนย์การค้า (`https://montri-th.github.io/shoppingcenter/?venue=<รหัสทะเบียน>`) และ `?venue=` ของเว็บนี้รับทั้ง uuid และรหัสทะเบียน — การเชื่อมเป็นรหัสเดียวกัน ไม่ใช่ชื่อ
- **รายการเดียวต่ออาคาร (เจ้าของ 29 ต.ค. 2569 19:47 “ร้านเครือ กับ ร้านในอาคาร (sheet 1+sheet2) ให้แสดงรวมกันไม่ต้องแยก”):** ชีต Fitness มีสาขาชุดเดียว (`02_STORES`; README ของชีตระบุว่า 03/08/09 เป็นมุมมองของข้อมูลเดิม ห้ามบวกเป็นสาขาเพิ่ม) จึงตีความว่า สาขาที่ชีตผูกกับศูนย์การค้า (venue id) และสาขาที่จุดตกในอาคารที่ตำแหน่งของศูนย์การค้านั้นตก แสดงในรายการเดียว ไม่นับซ้ำ ทั้งในหน้าศูนย์การค้า หน้าอาคาร ตาราง และหมุด · ในข้อมูลรุ่นนี้มีสาขาที่ไม่ได้ผูกแต่จุดของตัวเองอยู่ห่างจุดของศูนย์การค้า 22 ม. (Gateway Ekamai) ถ้าจุดตกในอาคารเดียวกันบนแผนที่ หน้าศูนย์นั้นจะรวมไว้ในรายการเดียว (เชื่อมตอนแสดงผล ไม่ใช่การผูกในชีต)

## 7.1 โลโก้

- **บทบาท:** ระบุแบรนด์หรือศูนย์การค้าข้างชื่อเท่านั้น ชื่อยังเป็นตัวระบุหลักเสมอ (เจ้าของ 29 ต.ค. 2569 23:32 “เอา logo + brand ขึ้นก่อน ชื่อสาขา”) เครื่องหมายเป็นของเจ้าของแต่ละราย การแสดงไม่ใช่การรับรองหรือใบอนุญาตเครื่องหมายการค้า
- **ทะเบียนโลโก้แบรนด์** (`scripts/build-logos.py` → `data/brand-logos.json`): หนึ่งไฟล์ต่อแบรนด์จาก `16_BRAND_LOGOS` (126 แบรนด์ · ทางการ 124 · รอง 2 · บทบาท `brand_primary` 122 · `shared_chain_banner` 4 คือโลโก้ร่วมของเครือที่แบรนด์ใช้) ไฟล์ทุกไฟล์ตรง SHA-256 ในแท็บ
- **วิธีแสดง:** ใช้ไบต์ต้นฉบับเมื่อขนาดพอดีจอ (SVG หรือภาพ ≤ 512 px และ ≤ 64 KB) — 62 ไฟล์ · ที่เหลือย่อขนาดแบบคงสัดส่วนเป็น 384 px (WebP) และ/หรือตัดขอบใสที่กว้างเกิน (เหลือขอบ 6%) หรือเติม namespace ให้ SVG — บันทึกวิธีและ SHA-256 ของต้นฉบับกับไฟล์ที่ได้ทุกไฟล์ (`logos[].transformations`, `logos[].original`) ไม่เปลี่ยนสี ไม่ครอปตัวอักษร ไม่วาดใหม่ ไม่ขยาย
- **พื้นหลัง:** ตามพื้นที่คลังโลโก้แนะนำ (สว่าง `#FFFFFF` · มืด `#182327` · กลาง `#7B877D`) เหมือนกันทั้งธีมสว่างและมืด · 3 แบรนด์ (The Cult Fitness Society, Red Rock Canyon Climbing Gym, Fitness7) คลังระบุพื้นสว่างแต่ส่วนสีขาวของโลโก้หายบนพื้นสว่าง จึงแสดงบนพื้นมืดและบันทึกเหตุผลไว้ใน `surfaceNote` (รายงานทีมวิจัยให้แก้คลัง)
- **โลโก้ศูนย์การค้า:** 56 ไฟล์ของศูนย์ที่ผูกในชุดนี้จาก snapshot ของทะเบียน ตรวจ SHA-256 ตอนคัดลอก · ใช้ไบต์เดิมเมื่อขนาดพอดีจอ 36 ไฟล์ · 20 ไฟล์ย่อขนาดแบบคงสัดส่วนตามกติกาเดียวกับโลโก้แบรนด์ (บันทึก SHA-256 ของต้นฉบับ `logos[].original`)
- ไม่เผยแพร่ลิงก์ Drive หรือบันทึกของคลังโลโก้ เผยแพร่เฉพาะหน้าแหล่งสาธารณะ https ของโลโก้ (`sourcePage`, `assetURL`)

## 8. สถานะค่า (EVID-05) และการแสดงผล

| สถานะ | ความหมายในชุดนี้ | การแสดงผล |
|---|---|---|
| measured | ≥ 1 แถวตามตัวกรอง | สีของ `count` ตามลำดับค่า (ระดับ 0–40) |
| measured_zero | ไม่มีแบรนด์ใดในชุดข้อมูลที่หน้าทางการระบุสาขาในพื้นที่นั้น ณ วันตรวจ (ตามตัวกรอง) — 0 ของชุดข้อมูลนี้ ไม่ใช่การสำรวจทุกฟิตเนส | สีระดับ 0 + เส้นขอบ zero (`--data-zero-outline`) ในแผงบอกว่าตัวกรองเดียวกันพบที่ใด |
| no_data | ไม่ใช้ (ทุกเขต/อำเภออยู่ในขอบเขตการค้นของชุดข้อมูล) — การแสดงผลแบบลายเส้นยังคงไว้ตามสัญญา | ลายเส้นทแยง |
| out_of_scope | ไม่เกิดในชุดนี้ | ไม่วาด |

แถวที่ไม่มีเขต/อำเภอหรือไม่มีจังหวัด **ไม่ถูกแสดงเป็นศูนย์ในพื้นที่ใด** — บอกเป็นจำนวนแยกในแผงและกราฟ

## 9. ข้อมูลที่ไม่คัดลอก

ไม่คัดลอกที่อยู่ ชั้น/ห้อง เบอร์โทร ลิงก์แผนที่ สตริงพิกัดดิบ บันทึกของนักวิจัย (`notes`, `company_research_note`, `coverage_note`) id แหล่งภายใน ข้อความ README ของชีต รายชื่อ sister brand และลิงก์ชีตหรือ registry ภายใน (`meta.excludedFields`) ลิงก์ที่เผยแพร่เป็นหน้าสาธารณะ https เท่านั้น สคริปต์ตัดลิงก์ Google Docs/Drive/Maps ทิ้งก่อนเขียนไฟล์ (`scripts/build-data.py#public_url`) ที่อยู่ใช้อ่านพื้นที่ตอนสร้างข้อมูลเท่านั้น สิ่งที่เผยแพร่คือคำบริหารที่จับคู่ได้ (`cols.words`) ไม่ใช่ที่อยู่

## 10. การสร้างไฟล์ข้อมูล

```bash
# ขอบเขต: base info + .meta.json receipts จาก API สาธารณะของ production และ checkout ของ montri-th/pharmacy
python3 -I scripts/build-boundaries.py --api <api-dir> --pharmacy <pharmacy-checkout> --ogd <opengisdata-dir> --out .
# จุดใกล้ขอบเขต (≤ 150 ม.): ดึง SUBDISTRICT geometry ความละเอียดเต็มพร้อมใบรับ
python3 -I scripts/fetch-edge-geometry.py --export <sheet-export> --pharmacy <pharmacy-checkout> --frames data/frames --index data/area-index.json --out <geometry-dir>
# สาขา แบรนด์ บริษัท (อินพุตภายใน: export ของชีตวิจัย .xlsx หรือโฟลเดอร์ CSV — ไม่อยู่ใน repository)
# --registry: checkout ของ montri-th/shoppingcenter ที่ commit ที่บันทึก (ศูนย์ที่ 07_SHOPPING_CENTERS ไม่มี + โลโก้ศูนย์)
python3 -I scripts/build-data.py --export <sheet-export> --api <api-dir> --ogd <opengisdata-dir> --geom <geometry-dir> --index data/area-index.json --frames data/frames --out . \
  --registry <shoppingcenter-checkout> --registry-commit <sha>
# โลโก้แบรนด์ (อินพุตภายใน: คลังโลโก้ + แท็บ 16_BRAND_LOGOS และ 01_BRANDS เป็น CSV)
python3 -I scripts/build-logos.py --library <logo-library-dir> --sheet <16_BRAND_LOGOS.csv> --brands <01_BRANDS.csv> --out .
# ใบรับของทุก response ที่ใช้
python3 -I scripts/build-receipts.py --api <api-dir> --geom <geometry-dir> --out .
```

สคริปต์หยุดเมื่อ: ตัวเลขหัวตารางไม่ตรงกับ README ของชีต · จำนวนแถวต่อแบรนด์ไม่ตรงกับ `14_NETWORK_COVERAGE` · จุดใกล้ขอบเขตไม่มี geometry ที่มีใบรับ · เขตของศูนย์การค้าไม่ตรงกับทะเบียน · แถวที่ผูกแล้วชี้ศูนย์ที่ไม่มีทั้งใน `07_SHOPPING_CENTERS` และ snapshot ของทะเบียน · uuid ในชีตไม่ตรงกับทะเบียน · ไฟล์โลโก้ไม่ตรง SHA-256 หรือแบรนด์ใดไม่มีแถวโลโก้

## 11. ข้อจำกัดที่ทราบ

- **ไม่ใช่สำมะโน:** 126 แบรนด์ที่คัดไว้ ฟิตเนสอิสระ ยิมในคอนโด/โรงแรม และแบรนด์นอกรายชื่อไม่อยู่ในชุดข้อมูล (Observed fact จากขอบเขตของชีต)
- **ระบุพื้นที่ได้ไม่ครบ:** 70 จาก 352 สาขาไม่มีเขต/อำเภอ (40 ระบุได้แค่จังหวัด · 29 ไม่ระบุจังหวัด · 1 ขัดกัน) — เช่น Fitness First ระบุเขตได้ 26 จาก 32 สาขา ข้อค้นพบแรกจึงบอกตัวหารเป็น “สาขาที่ระบุเขตได้” และบอกจำนวนที่เหลือ
- **จุดส่วนใหญ่เป็นของศูนย์การค้า:** 157 แถวมีจุด 129 แถวเป็นจุดอ้างอิงของศูนย์การค้า (ไม่ใช่หน้าร้าน) หมุดบนแผนที่จึงไม่ใช่ภาพการกระจายตัวทั้งหมด สีของเขต/อำเภอคือภาพที่ถูกต้อง
- **สถานะ = การแสดงบนหน้าทางการ ณ วันตรวจ** ไม่ใช่การเปิดจริงรายวัน ไม่ใช่จำนวนสมาชิก ขนาด ราคา หรือผู้ใช้บริการ
- **บริษัท:** ชื่อตามแหล่ง ยังไม่ได้ตรวจกับทะเบียนนิติบุคคล ช่องว่างไม่ใช่หลักฐานความเป็นอิสระ
- **ศูนย์การค้า:** ผูก 147 แถว · 19 แถวพักไว้จนทะเบียนแก้รายการซ้ำหรือกำกวม · 13 ศูนย์ที่ผูกแล้วทะเบียนยังไม่มีจุด และ 15 ศูนย์ยังไม่มี uuid ของ CityMETER (รายงานทีมทะเบียน) · ทะเบียนระบุรหัสเขตไม่ตรงจุดของตัวเอง 2 แห่ง (ใช้จุดเป็นตัวตัดสิน)
- **โลโก้:** ใช้ระบุแบรนด์ ไม่ใช่หลักฐานสถานะหรือความเป็นเจ้าของ 4 แบรนด์ใช้โลโก้ร่วมของเครือ ไม่ใช่โลโก้เฉพาะแบรนด์ย่อย · ลิขสิทธิ์และเครื่องหมายการค้าเป็นของเจ้าของแต่ละราย
- **ปุ่ม Google (AI Mode · Maps · Street View):** เปิดนอกเดโม คำตอบเป็นของ Google อาจไม่ตรงกับหน้าทางการ ไม่ถูกนำกลับมาเป็นข้อมูล Street View ใกล้ศูนย์การค้าใช้จุดอ้างอิงของทะเบียน ไม่ใช่ทางเข้าร้าน
- **สิทธิ์การใช้ข้อมูล** ของแต่ละแหล่งยังต้องตรวจก่อนใช้เชิงพาณิชย์ (backlog N02 ใน `handoff/IMPLEMENTATION_PLAN.md`)
