/* global window */
// Mock data for 이동지도 — 서울/수도권 페르소나 분석

const SEEDED_REGIONS = [
  // ── 서울 ──
  { id: "mapo", group: "seoul", name: "마포구", nameEn: "Mapo", available: true,
    avgPrice: "13.8억", deltaY: "+4.2%",
    keywords: ["미디어", "출판", "젊은 직장인"],
    blurb: "출판·미디어·콘텐츠 산업이 만든 실용적 도시. 합정·연남·망원의 라이프스타일 벨트." },
  { id: "songpa", group: "seoul", name: "송파구", nameEn: "Songpa", available: true,
    avgPrice: "21.4억", deltaY: "+6.7%",
    keywords: ["대단지", "학군", "잠실 트라이앵글"],
    blurb: "잠실 트라이앵글이 만들어내는 가족 단위 도시. 학군과 인프라의 안정 자산." },
  { id: "seongdong", group: "seoul", name: "성동구", nameEn: "Seongdong", available: true,
    avgPrice: "16.9억", deltaY: "+8.1%",
    keywords: ["성수", "한강", "신흥"],
    blurb: "성수가 끌어올린 신흥 자산권. 한강과 산업 유산이 만든 도시 텍스처." },
  { id: "gangdong", group: "seoul", name: "강동구", nameEn: "Gangdong", available: true,
    avgPrice: "12.3억", deltaY: "+3.4%",
    keywords: ["고덕", "둔촌", "재건축"],
    blurb: "재건축 사이클이 만든 다음 챕터. 가족 회복형 매수자의 권역." },

  // ── 경기 ──
  { id: "gyeonggi-31023", group: "gyeonggi", name: "성남시 분당구", nameEn: "Bundang", available: true,
    avgPrice: "16.2억", deltaY: "+5.0%",
    keywords: ["판교", "테크", "1기 신도시"],
    blurb: "판교 테크밸리가 만든 신중산층 도시. 1기 신도시 재건축의 새 출발선." },
  { id: "gyeonggi-31014", group: "gyeonggi", name: "수원시 영통구", nameEn: "Yeongtong", available: true,
    avgPrice: "8.4억", deltaY: "+2.1%",
    keywords: ["광교", "삼성", "직주근접"],
    blurb: "광교의 학군과 삼성 직주근접이 받쳐주는 안정형 매수권." },
  { id: "gyeonggi-31101", group: "gyeonggi", name: "고양시 덕양구", nameEn: "Deogyang", available: true,
    avgPrice: "7.6억", deltaY: "+1.4%",
    keywords: ["창릉", "GTX-A", "실거주"],
    blurb: "GTX-A와 창릉 신도시가 동시에 진행되는 서울 서북부의 다음 페이지." },

  // ── 인천 ──
  { id: "incheon-23040", group: "incheon", name: "연수구", nameEn: "Yeonsu", available: true,
    avgPrice: "6.9억", deltaY: "+1.8%",
    keywords: ["송도", "국제도시", "GTX-B"],
    blurb: "송도 국제도시와 GTX-B 가 다시 그리는 해안형 신도시 자산권." },
];

// Build SEOUL_SHAPES from prebuilt path file (Seoul + Incheon + Gyeonggi).
const ALL_PATHS = (window.__SEOUL_PATHS__ || []).map(p => ({
  id: p.id,
  name: p.shortName || p.name,
  fullName: p.name,
  region: p.region,
  d: p.d,
  cx: p.cx,
  cy: p.cy,
}));
const SEOUL_SHAPES = ALL_PATHS;

const SEEDED_REGION_MAP = Object.fromEntries(SEEDED_REGIONS.map(region => [region.id, region]));

const FULL_PRICE_HINTS = {
  gangnam: 15.7, gangdong: 12.3, gangbuk: 7.8, gangseo: 10.1, gwanak: 8.6,
  gwangjin: 12.4, guro: 7.9, geumcheon: 7.2, nowon: 7.6, dobong: 7.3,
  dongdaemun: 9.2, dongjak: 12.0, mapo: 13.8, seodaemun: 10.6, seocho: 24.8,
  seongdong: 16.9, seongbuk: 10.4, songpa: 21.4, yangcheon: 12.1, yeongdeungpo: 11.5,
  yongsan: 22.6, eunpyeong: 8.1, jongno: 11.2, jung: 9.8, jungnang: 7.4,
  "gyeonggi-31010": 11.8, "gyeonggi-31011": 12.1, "gyeonggi-31012": 11.4, "gyeonggi-31014": 8.4,
  "gyeonggi-31020": 9.8, "gyeonggi-31021": 9.5, "gyeonggi-31023": 16.2, "gyeonggi-31040": 7.9,
  "gyeonggi-31050": 11.1, "gyeonggi-31051": 12.4, "gyeonggi-31060": 7.4, "gyeonggi-31061": 7.4,
  "gyeonggi-31062": 7.6, "gyeonggi-31063": 7.5, "gyeonggi-31080": 6.4,
  "gyeonggi-31090": 6.4, "gyeonggi-31100": 11.3, "gyeonggi-31101": 7.6, "gyeonggi-31102": 11.2,
  "gyeonggi-31103": 12.0, "gyeonggi-31110": 6.1, "gyeonggi-31111": 18.4, "gyeonggi-31120": 7.3, "gyeonggi-31140": 5.8, "gyeonggi-31160": 11.4,
  "gyeonggi-31170": 13.0, "gyeonggi-31180": 13.2, "gyeonggi-31190": 8.7, "gyeonggi-31191": 12.6,
  "gyeonggi-31192": 15.4,
  "incheon-23010": 6.8, "incheon-23020": 5.4, "incheon-23030": 7.2, "incheon-23040": 6.9,
  "incheon-23050": 7.4, "incheon-23060": 6.5, "incheon-23070": 6.2, "incheon-23080": 7.0,
  "incheon-23310": 4.4, "incheon-23320": 3.4,
  // 읍면동 splits
  "gyeonggi-pt-0110": 6.8,
  "gyeonggi-pt-0120": 7.3,
  "gyeonggi-pt-0130": 6.9,
  "gyeonggi-pt-0310": 6.4,
  "gyeonggi-pt-0320": 5.9,
  "gyeonggi-pt-0330": 6.5,
  "gyeonggi-pt-0340": 6,
  "gyeonggi-pt-0140": 6.4,
  "gyeonggi-pt-0370": 6.7,
  "gyeonggi-pt-d0": 8.2,
  "gyeonggi-pt-d1": 7.2,
  "gyeonggi-pt-d2": 7.3,
  "gyeonggi-hs-41591": 6.4,
  "gyeonggi-hs-41593": 7.8,
  "gyeonggi-hs-41595": 9.2,
  "gyeonggi-hs-41597": 12.6,
  "gyeonggi-ny-0110": 6,
  "gyeonggi-ny-0120": 5.6,
  "gyeonggi-ny-0130": 6.1,
  "gyeonggi-ny-0140": 5.7,
  "gyeonggi-ny-0150": 5.2,
  "gyeonggi-ny-0310": 5.6,
  "gyeonggi-ny-0160": 5.8,
  "gyeonggi-ny-0340": 5.3,
  "gyeonggi-ny-0350": 4.8,
  "gyeonggi-ny-d0": 8.2,
  "gyeonggi-ny-d1": 6.7,
  "gyeonggi-ny-d2": 6.7,
  "gyeonggi-pj-0110": 7,
  "gyeonggi-pj-0120": 6.5,
  "gyeonggi-pj-0130": 7.1,
  "gyeonggi-pj-0150": 6.2,
  "gyeonggi-pj-0310": 6.6,
  "gyeonggi-pj-0320": 6.1,
  "gyeonggi-pj-0350": 5.8,
  "gyeonggi-pj-0360": 6.3,
  "gyeonggi-pj-0370": 5.9,
  "gyeonggi-pj-0390": 6,
  "gyeonggi-pj-d0": 9.4,
  "gyeonggi-pj-d1": 8.8,
  "gyeonggi-pj-d2": 7.5,
  "gyeonggi-gp-0110": 7.1,
  "gyeonggi-gp-0120": 7.7,
  "gyeonggi-gp-0130": 7.2,
  "gyeonggi-gp-0340": 6.4,
  "gyeonggi-gp-0350": 6.9,
  "gyeonggi-gp-0360": 6.5,
  "gyeonggi-gp-d0": 9.2,
  "gyeonggi-gp-d1": 8,
  "gyeonggi-gp-d2": 8,
  "gyeonggi-gj-0120": 4.8,
  "gyeonggi-gj-0140": 4.9,
  "gyeonggi-gj-0340": 4.5,
  "gyeonggi-gj-0350": 4,
  "gyeonggi-gj-0360": 4.6,
  "gyeonggi-gj-0380": 4.7,
  "gyeonggi-gj-d0": 5,
  "gyeonggi-gj-d1": 5.1,
  "gyeonggi-gj-d2": 5.1,
  "gyeonggi-yj-0110": 4.5,
  "gyeonggi-yj-0310": 4.1,
  "gyeonggi-yj-0320": 3.6,
  "gyeonggi-yj-0330": 4.2,
  "gyeonggi-yj-0340": 3.8,
  "gyeonggi-yj-d0": 5.5,
  "gyeonggi-yj-d1": 5.6,
  "gyeonggi-yj-d2": 4.6,
  "gyeonggi-pc-0110": 3,
  "gyeonggi-pc-0310": 2.6,
  "gyeonggi-pc-0320": 3.1,
  "gyeonggi-pc-0330": 2.7,
  "gyeonggi-pc-0340": 3.3,
  "gyeonggi-pc-0350": 2.8,
  "gyeonggi-pc-0360": 3.4,
  "gyeonggi-pc-0370": 2.9,
  "gyeonggi-pc-0380": 3.5,
  "gyeonggi-pc-0390": 3,
  "gyeonggi-pc-0400": 3.1,
  "gyeonggi-pc-0410": 2.6,
  "gyeonggi-pc-0510": 4,
  "gyeonggi-pc-0520": 4.6,
  "gyeonggi-yj2-0110": 3.7,
  "gyeonggi-yj2-0310": 2.3,
  "gyeonggi-yj2-0390": 2.7,
  "gyeonggi-yj2-0330": 2.4,
  "gyeonggi-yj2-0340": 3,
  "gyeonggi-yj2-0350": 2.5,
  "gyeonggi-yj2-0360": 3.1,
  "gyeonggi-yj2-0370": 2.6,
  "gyeonggi-yj2-0380": 3.2,
  "gyeonggi-yj2-0510": 3.7,
  "gyeonggi-yj2-0520": 4.3,
  "gyeonggi-yj2-0530": 3.8,
  "gyeonggi-ic-0110": 4.3,
  "gyeonggi-ic-0120": 4.8,
  "gyeonggi-ic-0310": 3.9,
  "gyeonggi-ic-0320": 4.4,
  "gyeonggi-ic-0330": 4,
  "gyeonggi-ic-0340": 3.5,
  "gyeonggi-ic-0350": 4.1,
  "gyeonggi-ic-0360": 3.6,
  "gyeonggi-ic-0370": 4.2,
  "gyeonggi-ic-0380": 3.7,
  "gyeonggi-ic-0510": 5.3,
  "gyeonggi-ic-0520": 4.8,
  "gyeonggi-ic-0530": 5.4,
  "gyeonggi-ic-0540": 4.9,
  "gyeonggi-as-0110": 5.9,
  "gyeonggi-as-0310": 5.5,
  "gyeonggi-as-0320": 5,
  "gyeonggi-as-0330": 5.6,
  "gyeonggi-as-0340": 5.1,
  "gyeonggi-as-0350": 5.7,
  "gyeonggi-as-0360": 5.2,
  "gyeonggi-as-0380": 5.3,
  "gyeonggi-as-0390": 5.9,
  "gyeonggi-as-0400": 5,
  "gyeonggi-as-0410": 5.5,
  "gyeonggi-as-0420": 5.1,
  "gyeonggi-as-0510": 6.9,
  "gyeonggi-as-0520": 6.4,
  "gyeonggi-as-0530": 7,
  "gyeonggi-sh-d0": 12.3,
  "gyeonggi-sh-d1": 12.4,
  "gyeonggi-sh-d2": 12.4,
  "gyeonggi-sh-d3": 12.5,
  "gyeonggi-sh-d4": 6.8,
  "gyeonggi-yc-0110": 3.3,
  "gyeonggi-yc-0120": 2.8,
  "gyeonggi-yc-0310": 2.9,
  "gyeonggi-yc-0320": 2.4,
  "gyeonggi-yc-0340": 2.5,
  "gyeonggi-yc-0350": 2.1,
  "gyeonggi-yc-0360": 2.6,
  "gyeonggi-yc-0370": 2.2,
  "gyeonggi-yc-0380": 2.7,
  "gyeonggi-yc-0330": 2,
  "gyeonggi-gp2-0110": 3.2,
  "gyeonggi-gp2-0310": 2.8,
  "gyeonggi-gp2-0320": 2.3,
  "gyeonggi-gp2-0330": 2.9,
  "gyeonggi-gp2-0360": 2.6,
  "gyeonggi-gp2-0350": 3,
  "gyeonggi-yp-0110": 3,
  "gyeonggi-yp-0310": 2.6,
  "gyeonggi-yp-0320": 3.1,
  "gyeonggi-yp-0330": 2.7,
  "gyeonggi-yp-0340": 3.3,
  "gyeonggi-yp-0350": 2.8,
  "gyeonggi-yp-0360": 3.4,
  "gyeonggi-yp-0370": 2.9,
  "gyeonggi-yp-0380": 3.5,
  "gyeonggi-yp-0390": 3,
  "gyeonggi-yp-0400": 3.1,
  "gyeonggi-yp-0410": 2.6,
};

const GROUP_META = {
  seoul: {
    tag: ["서울권", "직주근접", "압축된 생활권"],
    blurb: "서울 안에서 생활 반경과 자산 위계가 빠르게 갈리는 대표 권역.",
  },
  gyeonggi: {
    tag: ["수도권 외곽", "확장형 실거주", "노선 의존"],
    blurb: "GTX·재건축·대단지 수요가 섞이면서 자산 격차가 빠르게 벌어지는 수도권 확장 권역.",
  },
  incheon: {
    tag: ["해안권", "신도시", "노선 기대"],
    blurb: "국제도시와 구도심이 함께 움직이며 가격 방향이 나뉘는 해안형 생활권.",
  },
};

function syntheticPriceForId(id, group) {
  if (FULL_PRICE_HINTS[id] != null) return FULL_PRICE_HINTS[id];
  let h = 0;
  for (let i = 0; i < id.length; i++) h = (h * 31 + id.charCodeAt(i)) >>> 0;
  const base = group === "seoul" ? 9.5 : group === "incheon" ? 5.8 : 6.8;
  return Math.round((base + ((h % 90) / 20)) * 10) / 10;
}

function syntheticDelta(id) {
  let h = 0;
  for (let i = 0; i < id.length; i++) h = (h * 17 + id.charCodeAt(i)) >>> 0;
  const raw = (((h % 60) - 10) / 10).toFixed(1);
  return `${Number(raw) >= 0 ? "+" : ""}${raw}%`;
}

function regionLabelKeywords(shape) {
  const meta = GROUP_META[shape.region] || GROUP_META.gyeonggi;
  if (shape.name.includes("구")) return [shape.name.replace("구", ""), ...meta.tag.slice(0, 2)];
  if (shape.name.includes("시")) return [shape.name.replace("시", ""), "시 단위", meta.tag[1]];
  if (shape.name.includes("군")) return [shape.name.replace("군", ""), "군 단위", meta.tag[2]];
  return meta.tag;
}

function regionBlurb(shape) {
  const meta = GROUP_META[shape.region] || GROUP_META.gyeonggi;
  const coarse = shape.name.includes("시") && !shape.name.includes("구")
    ? "현재 시 단위 경계 기준으로 먼저 보여주고 있습니다."
    : "";
  return `${shape.fullName || shape.name}는 ${meta.blurb} ${coarse}`.trim();
}

const REGIONS = ALL_PATHS.map(shape => {
  const seeded = SEEDED_REGION_MAP[shape.id];
  if (seeded) return { ...seeded, featured: true, available: true };
  const price = syntheticPriceForId(shape.id, shape.region);
  return {
    id: shape.id,
    group: shape.region,
    name: shape.fullName || shape.name,
    nameEn: shape.name,
    available: true,
    featured: false,
    avgPrice: `${price.toFixed(1)}억`,
    deltaY: syntheticDelta(shape.id),
    keywords: regionLabelKeywords(shape),
    blurb: regionBlurb(shape),
  };
});

/* ============================================================
   PERSONAS — distinct per region
   ============================================================ */
const PERSONAS = {
  __generic: [
    { id:"steady_family", emoji:"🏡", name:"실거주 중심 가족", tagline:"동선과 학군을 먼저 본다",
      quote:"너무 멀리 가지 않으면서도 한 단계 나아가고 싶어요.",
      tags:["가족", "실거주", "안정 선호"] },
    { id:"upward_worker", emoji:"💼", name:"상향 이동 직장인", tagline:"출퇴근과 자산을 같이 계산한다",
      quote:"통근이 줄고 자산이 오르면 제일 좋죠.",
      tags:["직장인", "갈아타기", "노선 민감"] },
    { id:"first_home", emoji:"🔑", name:"첫 매수 준비층", tagline:"서울과 수도권의 경계를 고민한다",
      quote:"지금 살 수 있는 곳과 나중에 가고 싶은 곳이 달라요.",
      tags:["첫 매수", "예산 제한", "확장 탐색"] },
    { id:"upgrade_owner", emoji:"📈", name:"갈아타기 보유자", tagline:"현재 집을 발판으로 다음 위계를 본다",
      quote:"지금 집을 잘 팔아야 다음 집이 보입니다.",
      tags:["보유자", "상향 이동", "가격 민감"] },
    { id:"lifestyle_dink", emoji:"🍷", name:"라이프스타일 DINK", tagline:"생활 밀도와 도시 감도를 중시한다",
      quote:"집값만 맞는 곳보다, 내가 살아질 동네가 더 중요해요.",
      tags:["DINK", "도시 취향", "선호도 우선"] },
    { id:"retiring_senior", emoji:"🌅", name:"은퇴 전후 다운사이저", tagline:"넓은 집을 팔고 작은 집에서 현금을 쥔다",
      quote:"평수 줄이면 노후 자금이 남아요. 그게 진짜 전략이죠.",
      tags:["55세+", "다운사이징", "현금 확보"] },
    { id:"return_local", emoji:"🧳", name:"귀향형 재정착자", tagline:"서울 생활을 접고 돌아온 사람",
      quote:"오래 살았던 동네로 돌아오니까 물가가 달라요.",
      tags:["귀향", "재정착", "생활비 절감"] },
    { id:"young_investor", emoji:"🎯", name:"소액 투자형 청년", tagline:"실거주보다 수익률을 먼저 계산한다",
      quote:"월세 받으면서 전세로 사는 게 지금은 맞아요.",
      tags:["20대 후반", "소액 투자", "갭투자 탐색"] },
  ],
  // 마포: 미디어/출판/콘텐츠 도시 — 5명
  mapo: [
    { id:"publisher", emoji:"📚", name:"출판사 13년차 PM", tagline:"인쇄소와 카페 사이에서 흔들린다",
      quote:"아이가 초등학교에 들어가면, 이 동네에서 더 살 수 있을까.",
      tags:["38세","맞벌이","전세→매수"] },
    { id:"studio", emoji:"🎬", name:"영상 스튜디오 대표", tagline:"성수가 부럽지만, 합정의 골목이 좋다",
      quote:"스튜디오를 옮기면 사람이 안 와요.",
      tags:["41세","1인 법인","월세 60만"] },
    { id:"designer", emoji:"✏️", name:"프리랜스 그래픽 디자이너", tagline:"동네에서 일하고 동네에서 마신다",
      quote:"여기 떠나면, 나라는 사람이 좀 흐려질 것 같아.",
      tags:["33세","1인","월세→전세"] },
    { id:"couple", emoji:"🍷", name:"DINK 부부", tagline:"도시의 밀도를 자산처럼 쓴다",
      quote:"주말마다 한남 가는 게 좀 지겨워졌어.",
      tags:["35·36세","2인","갭투자 검토"] },
    { id:"founder", emoji:"🥂", name:"초기 단계 파운더", tagline:"투자유치만 되면 한남으로 갈 사람",
      quote:"다음 라운드 끝나면, 바로 강 건너야지.",
      tags:["32세","Pre-A","지분 매도 후 매수"] },
    { id:"single-mom", emoji:"🛡️", name:"혼자 키우는 워킹맘", tagline:"학교·병원·어린이집 동선이 곧 생존",
      quote:"이 동네가 안전하고 모든 게 걸어서 되니까 버틸 수 있어요.",
      tags:["39세","자녀 1","전세 갱신"] },
    { id:"night-worker", emoji:"🌙", name:"야간 교대 간호사", tagline:"출퇴근 20분이 삶의 전부",
      quote:"밤번 끝나고 택시비가 아까워서 근처로 왔어요.",
      tags:["31세","3교대","병원 도보 통근"] },
    { id:"retiree-return", emoji:"🎣", name:"은퇴 후 도심 복귀", tagline:"세종시 갔다가 다시 서울로",
      quote:"병원이랑 문화생활은 결국 서울이더라고.",
      tags:["62세","연금 생활","소형 매수"] },
  ],
  // 송파: 대단지·학군·가족형 — 8명
  songpa: [
    { id:"jamsil-mom", emoji:"📐", name:"잠실 학부모", tagline:"학원가 반경 800m 안에서 모든 게 끝난다",
      quote:"이 동네 떠나면 아이 학원 동선이 다 깨져요.",
      tags:["44세","자녀 2","리센츠 입주 7년"] },
    { id:"asiad-dad", emoji:"🏟️", name:"아시아드 거주 14년", tagline:"이사 한 번으로 평수만 늘리고 싶다",
      quote:"단지를 바꾸는 게 아니라 평수를 바꾸고 싶어요.",
      tags:["49세","외벌이","대형 평형 검토"] },
    { id:"newcomer", emoji:"🚚", name:"강북에서 막 넘어온 부부", tagline:"잠실 인프라에 적응 중",
      quote:"여긴 도시가 다른 게 아니라 시간이 다른 것 같아요.",
      tags:["38·39세","자녀 1","갈아타기 1차 완료"] },
    { id:"finance", emoji:"📊", name:"여의도 출퇴근 자산가", tagline:"강남 안 가도 충분하다고 믿는다",
      quote:"강남 가격 주고 강남 안 살 이유가 있죠.",
      tags:["46세","자산운용업","대형 평형"] },
    { id:"olympic", emoji:"🏊", name:"올림픽선수기자촌 출신", tagline:"재건축이 끝나면 돌아올 사람",
      quote:"여긴 다음 사이클까지 들고 갈 자산이에요.",
      tags:["52세","자녀 2 출가","재건축 대기"] },
    { id:"upgrader", emoji:"🛗", name:"분당에서 갈아탄 임원", tagline:"마지막 한 번의 이사",
      quote:"이걸로 진짜 끝이라고 생각하고 들어왔어요.",
      tags:["55세","대기업 임원","엔드게임 매수"] },
    { id:"foreign-spouse", emoji:"🌐", name:"외국인 배우자 가족", tagline:"국제학교와 영어 환경이 기준",
      quote:"잠실은 외국인 커뮤니티가 있어서 아내가 편해해요.",
      tags:["42세","주재원 출신","자녀 2"] },
    { id:"single-senior", emoji:"🏥", name:"독거 시니어", tagline:"병원 한 블록, 마트 두 블록",
      quote:"아파서 누우면 119가 5분 안에 와야 해요.",
      tags:["68세","독거","소형 전세"] },
  ],
  // 성동: 신흥/한강/크리에이티브 — 8명
  seongdong: [
    { id:"seongsu-pm", emoji:"☕", name:"성수동 브랜드 PM", tagline:"카페와 사옥 사이에서 산다",
      quote:"여기서 사는 거랑 일하는 거랑 분리가 잘 안 돼요.",
      tags:["34세","DINK","서울숲 전세"] },
    { id:"trinity-mom", emoji:"🎻", name:"트리니티 학군 학부모", tagline:"옥수에서 시작해 한남을 본다",
      quote:"학교는 여기, 그 다음은 강 건너편을 봐요.",
      tags:["41세","자녀 2","트리니티 진학"] },
    { id:"ip-lawyer", emoji:"⚖️", name:"성수 사옥 출퇴근 변호사", tagline:"강북에 발을 디딘 강남 클라이언트",
      quote:"클라이언트 절반은 한강 남쪽에 있어요.",
      tags:["39세","외벌이","아크로 검토"] },
    { id:"f&b-owner", emoji:"🍽️", name:"F&B 오너", tagline:"성수에서 만들고 한남에서 판다",
      quote:"임대료가 매장보다 집이 더 무서워요.",
      tags:["43세","2호점 운영","강 건너 매수 욕망"] },
    { id:"dink", emoji:"🚲", name:"옥수 DINK", tagline:"한강이 곧 거실인 부부",
      quote:"이 야경을 다른 데서 다시 살 수 있을까.",
      tags:["37·36세","2인","아파트 갈아타기"] },
    { id:"teacher", emoji:"📖", name:"중학교 교사", tagline:"전근 걱정 없이 한 곳에 눌러앉고 싶다",
      quote:"전근이 서울 안이면 괜찮은데, 경기 가면 끝이에요.",
      tags:["36세","공무원","전세→매수 전환"] },
    { id:"foreigner-studio", emoji:"🎨", name:"외국인 아티스트", tagline:"성수의 작업실이 비자의 이유",
      quote:"이 동네를 떠나면 한국에 있을 이유가 줄어요.",
      tags:["29세","E-7 비자","성수 작업실"] },
    { id:"delivery-rider", emoji:"🛵", name:"배달 플랫폼 라이더", tagline:"구역이 곧 월급이다",
      quote:"왕십리 반경 3km가 제 사무실이에요.",
      tags:["27세","플랫폼 노동","원룸 월세"] },
  ],
  // 강동: 재건축 사이클·가족 회복형 — 8명
  gangdong: [
    { id:"godeok-newbuild", emoji:"🏗️", name:"고덕 신축 5년차", tagline:"신축의 단맛을 본 가족",
      quote:"신축을 한 번 살아보면 다시 못 가요.",
      tags:["43세","자녀 1","고덕 그라시움"] },
    { id:"dunchon-old", emoji:"🌳", name:"둔촌 30년 거주", tagline:"올림픽파크포레온이 인생의 마무리",
      quote:"여기서 결혼하고 여기서 손주 봤어요.",
      tags:["61세","은퇴 직전","입주 대기"] },
    { id:"return-mom", emoji:"🥡", name:"강남에서 돌아온 워킹맘", tagline:"가성비와 평수의 절충안",
      quote:"강남 30평이냐 여기 40평이냐, 여기를 골랐어요.",
      tags:["40세","맞벌이","갈아타기 2차"] },
    { id:"dual-income", emoji:"🚇", name:"9호선 의존형 직장인 부부", tagline:"강남까지 30분이면 됐다",
      quote:"이 동선이 깨지면 우리 둘 다 무너져요.",
      tags:["35·34세","DINK","둔촌 전세"] },
    { id:"investor", emoji:"📈", name:"재건축 투자자", tagline:"사이클을 두 번째 타는 매수자",
      quote:"이번엔 거주 안 합니다. 사이클만 봐요.",
      tags:["48세","다주택자","갭투자"] },
    { id:"grandma-care", emoji:"👵", name:"손주 돌봄 조부모", tagline:"딸네 옆에서 출퇴근하듯 육아한다",
      quote:"손주 어린이집 데려다주려면 걸어서 10분 안이어야 해요.",
      tags:["64세","은퇴","소형 전세"] },
    { id:"commute-couple", emoji:"🚆", name:"서울 출퇴근 신혼", tagline:"강동이 첫 집의 마지노선",
      quote:"5호선 끝이라도 서울 안이면 됐어요.",
      tags:["30·29세","맞벌이","첫 매수"] },
    { id:"pet-family", emoji:"🐕", name:"반려견 중심 가족", tagline:"공원과 산책로가 평수보다 중요하다",
      quote:"대형견 키우려면 일층이나 마당이 있어야 하는데, 여긴 하남 가기도 좋아요.",
      tags:["37세","자녀 없음","동물 3마리"] },
  ],
  // 분당: 판교 테크 + 1기 신도시 재건축 — 8명
  "gyeonggi-31023": [
    { id:"pangyo-eng", emoji:"💻", name:"판교 7년차 시니어 엔지니어", tagline:"분당에 산다는 자부심",
      quote:"강남 가는 거 아니면 여기 끝까지 살아요.",
      tags:["38세","외벌이","서현 전세"] },
    { id:"bundang-mom", emoji:"🎒", name:"분당 학부모", tagline:"학원가가 자산이다",
      quote:"학원가가 사라지면 분당이 분당이 아니에요.",
      tags:["45세","자녀 2","수내동"] },
    { id:"redevelop", emoji:"🏚️", name:"이매동 재건축 대기자", tagline:"30년 아파트의 다음 30년",
      quote:"우리 단지가 1번 타자가 됐으면 좋겠어요.",
      tags:["57세","외벌이","재건축 1차"] },
    { id:"founder-bundang", emoji:"🚀", name:"스타트업 파운더", tagline:"강남 안 가도 되는 사람",
      quote:"투자자도 판교에 와서 만나요, 이젠.",
      tags:["36세","Series B","판교역세권"] },
    { id:"return-yongin", emoji:"🚙", name:"용인에서 갈아탄 가족", tagline:"한 번 더 위로",
      quote:"용인은 차고 분당은 도시예요.",
      tags:["41세","자녀 2","정자동 매수"] },
    { id:"cafe-nomad", emoji:"☕", name:"카페 노마드 프리랜서", tagline:"판교 카페가 사무실이다",
      quote:"분당 카페 와이파이가 강남보다 빠르고 자리가 넓어요.",
      tags:["31세","IT 프리랜서","전세 원룸"] },
    { id:"hagwon-teacher", emoji:"🧮", name:"학원 강사 겸 원장", tagline:"학원가 생태계 안에서 산다",
      quote:"수내역 반경 500m가 제 시장이에요.",
      tags:["47세","수학학원","수내동 자가"] },
    { id:"dual-city", emoji:"🔄", name:"서울-분당 이중생활자", tagline:"평일은 서울, 주말은 분당",
      quote:"아이는 분당에서 키우고 나는 서울에서 벌어요.",
      tags:["44세","맞벌이","두 집 유지"] },
  ],
  // 영통: 광교 + 삼성 직주근접 — 8명
  "gyeonggi-31014": [
    { id:"samsung-eng", emoji:"🔬", name:"삼성 R&D 연구원", tagline:"회사가 도시를 결정한다",
      quote:"매탄에서 광교로, 광교에서 어디로 갈까요.",
      tags:["35세","연구직","광교 매수"] },
    { id:"gwanggyo-mom", emoji:"🌷", name:"광교 학부모", tagline:"호수공원이 학군의 일부다",
      quote:"여기 학교가 강남보다 나아요, 진짜로.",
      tags:["42세","자녀 2","광교중앙"] },
    { id:"bundang-want", emoji:"🛣️", name:"분당을 보는 영통 거주자", tagline:"한 칸 더 위로",
      quote:"여기서 더 가려면 결국 분당이에요.",
      tags:["39세","맞벌이","갈아타기 검토"] },
    { id:"med-prof", emoji:"🩺", name:"수원 종합병원 의사", tagline:"광교의 안정성에 묶여 있다",
      quote:"병원 옆에 살아야 해서, 여기서 멀리 못 가요.",
      tags:["44세","외벌이","대형 평형"] },
    { id:"first-buy", emoji:"🔑", name:"첫 매수 부부", tagline:"광교가 첫 정착지",
      quote:"여기가 우리한테 첫 진짜 집이에요.",
      tags:["33·32세","DINK","입주 1년"] },
    { id:"ajumma-trader", emoji:"📱", name:"주식·부동산 겸업 주부", tagline:"HTS와 부동산 앱을 동시에 본다",
      quote:"주식은 단타, 부동산은 장타. 둘 다 해야 살아요.",
      tags:["48세","자녀 1","광교 자가"] },
    { id:"factory-commute", emoji:"🏭", name:"수원 산단 출퇴근자", tagline:"공장과 아파트 사이 20분",
      quote:"회사가 산단이라 영통 아니면 화성이에요.",
      tags:["36세","제조업","매탄동 전세"] },
    { id:"overseas-return", emoji:"✈️", name:"해외 주재 복귀 가족", tagline:"광교가 한국판 교외 느낌",
      quote:"미국 서버브 느낌이 나서 적응이 빨랐어요.",
      tags:["43세","대기업 복귀","자녀 2"] },
  ],
  // 덕양: GTX-A·창릉 — 8명
  "gyeonggi-31101": [
    { id:"gtx-commuter", emoji:"🚄", name:"GTX-A 베타 통근자", tagline:"30분이면 강남이다",
      quote:"이 한 노선이 우리 집값을 바꿨어요.",
      tags:["37세","서울 강남 출근","원흥 매수"] },
    { id:"changneung", emoji:"🏘️", name:"창릉 청약 당첨자", tagline:"3기 신도시의 첫 입주민",
      quote:"여기가 5년 뒤에 어떻게 변할지 진짜 궁금해요.",
      tags:["34·33세","자녀 1","2027 입주"] },
    { id:"return-paju", emoji:"🚌", name:"파주에서 회귀한 가족", tagline:"서울에 다시 가까워지고 싶은",
      quote:"애 학교가 진짜 큰 문제예요.",
      tags:["40세","자녀 2","갈아타기"] },
    { id:"ilsan-want", emoji:"🌉", name:"일산을 부러워하는 덕양 거주자", tagline:"한강을 더 가까이",
      quote:"같은 고양시인데 동네 위계가 달라요.",
      tags:["46세","외벌이","갈아타기 2차"] },
    { id:"first-house", emoji:"🪴", name:"첫 내 집 마련 부부", tagline:"서울이 너무 멀었던",
      quote:"서울은 못 사고, 서울 옆을 골랐어요.",
      tags:["31·30세","DINK","원당 매수"] },
    { id:"kintex-worker", emoji:"🎪", name:"킨텍스 전시 기획자", tagline:"일산·고양이 직장이자 생활권",
      quote:"전시 시즌엔 새벽에 출근하니까 걸어서 갈 수 있어야 해요.",
      tags:["35세","계약직","삼송 전세"] },
    { id:"multi-gen", emoji:"🏠", name:"3대 동거 가족", tagline:"부모님과 같은 단지에 산다",
      quote:"같은 아파트 다른 동에 부모님이 계세요.",
      tags:["40세","자녀 1","부모 동거"] },
    { id:"uber-driver", emoji:"🚗", name:"플랫폼 택시 기사", tagline:"서울 접근성이 수입을 결정한다",
      quote:"자유로 타고 20분이면 서울이에요. 그게 생명줄이죠.",
      tags:["52세","자영업","자가 보유"] },
  ],
  // 인천 연수: 송도 국제도시 — 8명
  "incheon-23040": [
    { id:"songdo-bio", emoji:"🧬", name:"송도 바이오 직장인", tagline:"회사 옆에 사는 자부심",
      quote:"여긴 회사 끝나면 바로 집이에요.",
      tags:["33세","바이오 회사","송도 6공구"] },
    { id:"songdo-mom", emoji:"🍃", name:"국제학교 학부모", tagline:"한국 안의 외국 같은 동네",
      quote:"여기서는 서울을 일부러 갈 일이 별로 없어요.",
      tags:["41세","자녀 2","채드윅"] },
    { id:"gtx-b", emoji:"🚞", name:"GTX-B 대기자", tagline:"노선 하나에 인생을 건다",
      quote:"GTX-B가 진짜 뚫리면 여긴 다른 도시가 돼요.",
      tags:["38세","서울 출근","연수 2년차"] },
    { id:"yeonsu-old", emoji:"🏖️", name:"연수 구도심 30년", tagline:"송도를 옆에서 본 사람",
      quote:"우리 동네가 송도 덕에 같이 올라갔어요.",
      tags:["55세","외벌이","연수동"] },
    { id:"first-young", emoji:"🌊", name:"첫 매수 청년 부부", tagline:"바다가 보이는 첫 집",
      quote:"이 가격에 이 뷰는 서울엔 없잖아요.",
      tags:["32·31세","DINK","송도 1공구"] },
    { id:"airport-crew", emoji:"✈️", name:"인천공항 승무원", tagline:"공항버스 한 방에 출근 끝",
      quote:"새벽 픽업이 집 앞이라 여기 아니면 못 살아요.",
      tags:["29세","항공사","송도 전세"] },
    { id:"trade-worker", emoji:"🚢", name:"무역회사 물류 담당", tagline:"항만과 오피스의 중간 지점",
      quote:"인천항도 가깝고 서울 본사도 한 시간이에요.",
      tags:["37세","중견기업","연수동 자가"] },
    { id:"retired-couple", emoji:"🌺", name:"해안 은퇴 부부", tagline:"바다 보이는 노후가 꿈",
      quote:"서울에서 30년 살았으니, 이제 바다 보면서 살래요.",
      tags:["61·59세","은퇴","송도 소형 매수"] },
  ],
};

function parsePriceEok(region) {
  const n = parseFloat(String(region.avgPrice || "").replace(/[^\d.]/g, ""));
  return Number.isFinite(n) ? n : syntheticPriceForId(region.id, region.group);
}

function genericPersonaProfile(region) {
  const price = parsePriceEok(region);
  const name = region.name || "";

  if (/옹진|강화/.test(name)) return "incheon_island";
  if (region.group === "incheon") {
    if (/연수|송도/.test(name) || price >= 6.8) return "incheon_newtown";
    return "incheon_mixed";
  }
  if (region.group === "seoul") {
    return price >= 11.5 ? "seoul_core" : "seoul_edge";
  }
  if (/군$|읍|면/.test(name)) return "gyeonggi_rural";
  if (price >= 10.5) return "gyeonggi_core";
  if (price >= 7.0) return "gyeonggi_commuter";
  return "gyeonggi_value";
}

const GENERIC_PERSONA_VARIANTS = {
  seoul_core: {
    steady_family: {
      emoji:"🧭", name:"생활권 고수 가족", tagline:"학교와 돌봄을 유지한 채 한 칸만 올린다",
      quote:"서울 안에서 선은 넘지 않되, 아이한테 더 나은 단지를 주고 싶어요.",
      tags:["가족","실거주","학군","생활권 유지"],
    },
    upward_worker: {
      emoji:"💼", name:"직주근접 상향 직장인", tagline:"출근 10분과 자산 격차를 함께 계산한다",
      quote:"회사와 가까워지는 동시에, 지금보다 한 단계 위의 주소를 원해요.",
      tags:["직장인","출퇴근","갈아타기","노선 민감"],
    },
    first_home: {
      emoji:"🗝️", name:"서울 잔류 첫 매수층", tagline:"서울 밖으로 나가기 전 마지막 계산을 한다",
      quote:"서울을 포기할지 말지, 지금이 그 경계선 같아요.",
      tags:["첫 매수","예산 제한","신혼","서울 잔류"],
    },
    upgrade_owner: {
      emoji:"📈", name:"브랜드 단지 상향 보유자", tagline:"현재 집을 발판으로 다음 위계를 본다",
      quote:"지금 집을 잘 팔아야, 결국 가고 싶던 라인에 닿아요.",
      tags:["보유자","갈아타기","상향 이동","가격 민감"],
    },
    lifestyle_dink: {
      emoji:"🍸", name:"도시감도 DINK", tagline:"가격보다 동네의 분위기와 속도를 본다",
      quote:"집값이 아니라, 내가 어떤 사람으로 살아질지가 중요해요.",
      tags:["DINK","라이프","도시 감","선호도 우선"],
    },
    retiring_senior: {
      emoji:"🌆", name:"도심 잔류 다운사이저", tagline:"멀리 가지 않고 평수만 줄여 현금을 만든다",
      quote:"서울은 남기고 싶고, 관리비와 평수는 줄이고 싶어요.",
      tags:["시니어","다운사이징","현금 확보","노후"],
    },
    return_local: {
      emoji:"🪃", name:"도심 복귀 재정착자", tagline:"외곽 생활을 접고 다시 서울 생활권을 찾는다",
      quote:"다시 돌아오려 보니, 예전보다 서울이 훨씬 비싸졌어요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"📊", name:"소형 진입 투자 청년", tagline:"작게 들어가서 다음 사이클을 노린다",
      quote:"지금은 작게 들어가도, 서울 안이면 다음 기회가 보여요.",
      tags:["청년","소액 투자","수익","갭투자 탐색"],
    },
  },
  seoul_edge: {
    steady_family: {
      emoji:"🏫", name:"학군-동선 맞벌이 가족", tagline:"서울 안에서 아이와 출근을 동시에 지킨다",
      quote:"멀리 나가면 아이 일정이 깨지고, 남으면 집이 좁아져요.",
      tags:["가족","실거주","학군","맞벌이"],
    },
    upward_worker: {
      emoji:"🚇", name:"노선 민감 직장인", tagline:"환승 한 번을 줄이기 위해 이사를 계산한다",
      quote:"출근 15분이 줄면, 내가 사는 도시가 달라져요.",
      tags:["직장인","출퇴근","직주","노선 민감"],
    },
    first_home: {
      emoji:"🔑", name:"서울 끝선 첫 매수층", tagline:"서울과 수도권의 경계에서 첫 집을 고른다",
      quote:"서울 주소를 지킬 수 있을 때 들어가야 한다고 생각해요.",
      tags:["첫 매수","예산 제한","청년","첫 집"],
    },
    upgrade_owner: {
      emoji:"🏢", name:"단지 상향 보유자", tagline:"동네는 유지하고 단지의 격을 올린다",
      quote:"생활권은 익숙한데, 단지는 한 단계 더 가고 싶어요.",
      tags:["갈아타기","보유자","상향 이동","가격 민감"],
    },
    lifestyle_dink: {
      emoji:"🎧", name:"생활밀도 DINK", tagline:"강북과 강서 사이에서 취향을 고른다",
      quote:"조용한 동네보다, 내가 자주 나가는 동네에 가깝고 싶어요.",
      tags:["DINK","라이프","취향","도시 감"],
    },
    retiring_senior: {
      emoji:"🌿", name:"서울 생활 유지 시니어", tagline:"병원과 지하철 가까운 곳으로 작게 옮긴다",
      quote:"서울을 벗어나면 편할 줄 알았는데, 병원 생각하면 또 아니에요.",
      tags:["은퇴","시니어","다운사이징","의료 접근"],
    },
    return_local: {
      emoji:"🧳", name:"서울 재정착 가족", tagline:"수도권 바깥 생활을 접고 다시 서울로 붙는다",
      quote:"다시 서울 가까이 오고 싶지만, 예전처럼 중심부는 어렵죠.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"🎯", name:"역세권 베팅 청년", tagline:"작은 예산으로 다음 상승 구간을 노린다",
      quote:"지금 싸게 사는 것보다, 다음에 덜 후회할 곳이 중요해요.",
      tags:["청년","소액 투자","수익","역세권"],
    },
  },
  gyeonggi_core: {
    steady_family: {
      emoji:"🏘️", name:"신도시 정착 가족", tagline:"학군과 브랜드 단지를 함께 본다",
      quote:"서울만 아니면 된다는 말이, 이제는 틀린 것 같아요.",
      tags:["가족","실거주","학군","신도시"],
    },
    upward_worker: {
      emoji:"🧑‍💻", name:"판교-강남 출근층", tagline:"서울 대신 광역 생활권에서 상향 이동을 본다",
      quote:"서울 주소보다 중요한 건, 내 동선이 더 좋아지는 거예요.",
      tags:["직장인","출퇴근","직주근접","갈아타기"],
    },
    first_home: {
      emoji:"🪴", name:"광역권 첫 집 부부", tagline:"서울을 포기하는 대신 생활 수준을 지킨다",
      quote:"서울 대신 이 동네를 택한 만큼, 첫 집의 만족도는 높아야 해요.",
      tags:["첫 매수","신혼","예산 제한","실거주"],
    },
    upgrade_owner: {
      emoji:"📦", name:"생활권 상향 보유자", tagline:"분당·광교·과천 같은 다음 위계를 본다",
      quote:"이 동네도 나쁘지 않지만, 다음엔 조금 더 상징적인 곳을 보고 있어요.",
      tags:["보유자","갈아타기","상향 이동","자산 위계"],
    },
    lifestyle_dink: {
      emoji:"☕", name:"광역생활 DINK", tagline:"서울만큼 붐비지 않으면서 감도 있는 곳을 찾는다",
      quote:"꼭 서울이 아니어도, 세련된 생활권은 분명히 있거든요.",
      tags:["DINK","라이프","취향","생활 밀도"],
    },
    retiring_senior: {
      emoji:"🚶", name:"인프라 중심 다운사이저", tagline:"병원과 공원을 기준으로 다음 집을 고른다",
      quote:"멀리 가고 싶진 않고, 걷기 좋은 인프라가 제일 중요해요.",
      tags:["은퇴","시니어","다운사이징","의료 접근"],
    },
    return_local: {
      emoji:"🏠", name:"광역권 복귀형", tagline:"서울에서 밀려난 게 아니라 다시 생활권을 정리한다",
      quote:"돌아온다는 건 후퇴가 아니라, 생활비와 삶을 다시 맞추는 거예요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"📱", name:"호재 추적 투자 청년", tagline:"GTX와 재건축이 만나는 곳을 찾는다",
      quote:"교통 하나 바뀌면 자산 곡선이 아예 달라져요.",
      tags:["청년","소액 투자","GTX","수익"],
    },
  },
  gyeonggi_commuter: {
    steady_family: {
      emoji:"🚌", name:"통근형 실거주 가족", tagline:"서울 접근성과 학교를 동시에 맞춘다",
      quote:"회사와 학교 사이에서, 이 동네가 가장 덜 무너지는 선택이에요.",
      tags:["가족","실거주","학군","통근"],
    },
    upward_worker: {
      emoji:"🚄", name:"GTX 대기 직장인", tagline:"노선 하나가 내 다음 집을 바꾼다고 믿는다",
      quote:"지금은 멀어도, 노선이 열리면 완전히 다른 동네가 돼요.",
      tags:["직장인","출퇴근","GTX","상향 이동"],
    },
    first_home: {
      emoji:"🔑", name:"실속형 첫 매수층", tagline:"서울과 가격 차이를 계산하며 첫 집을 고른다",
      quote:"조금 멀어도, 첫 집은 결국 감당 가능한 곳이어야 하니까요.",
      tags:["첫 매수","예산 제한","신혼","확장 탐색"],
    },
    upgrade_owner: {
      emoji:"📦", name:"동네 안 갈아타기 보유자", tagline:"같은 축에서 한 칸 위를 노린다",
      quote:"멀리 가지 말고, 같은 생활권에서 조금 더 좋은 단지로 가고 싶어요.",
      tags:["보유자","갈아타기","상향 이동","생활권 유지"],
    },
    lifestyle_dink: {
      emoji:"🍷", name:"신도시 감도 DINK", tagline:"차로 움직이는 생활권 안에서 취향을 찾는다",
      quote:"서울만큼 촘촘하진 않아도, 내 생활 패턴에 맞는 곳은 있어요.",
      tags:["DINK","라이프","생활 밀도","취향"],
    },
    retiring_senior: {
      emoji:"🌅", name:"현금 확보 다운사이저", tagline:"넓은 집을 정리하고 관리 쉬운 집으로 간다",
      quote:"평수보다 관리비가 먼저 보이는 시기가 왔어요.",
      tags:["은퇴","다운사이징","현금 확보","노후"],
    },
    return_local: {
      emoji:"🧳", name:"수도권 복귀형", tagline:"서울 밖에 남되 익숙한 생활권으로 되돌아온다",
      quote:"다시 돌아와도, 예전보다 선택지가 훨씬 좁아졌어요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"🎯", name:"교통호재 베팅 청년", tagline:"아직 덜 오른 축을 찾는다",
      quote:"사람들이 다 안 볼 때 들어가는 게 제일 중요해요.",
      tags:["청년","소액 투자","교통 호재","수익"],
    },
  },
  gyeonggi_value: {
    steady_family: {
      emoji:"🏡", name:"가성비 실거주 가족", tagline:"무리하지 않고 넓이와 학교를 함께 본다",
      quote:"서울까지 욕심내면 안 되고, 그래도 아이 키우기 좋아야 해요.",
      tags:["가족","실거주","학군","안정 선호"],
    },
    upward_worker: {
      emoji:"🛣️", name:"장거리 출퇴근 직장인", tagline:"시간을 줄이기 위해 상향 이동을 고민한다",
      quote:"출근길이 너무 길어지면, 결국 집을 바꿔야 하더라고요.",
      tags:["직장인","출퇴근","갈아타기","직주근접"],
    },
    first_home: {
      emoji:"🪙", name:"예산 우선 첫 매수층", tagline:"무조건 감당 가능한 첫 집을 고른다",
      quote:"첫 집이 완벽할 수는 없지만, 적어도 버틸 수는 있어야 해요.",
      tags:["첫 매수","예산 제한","첫 집","실속"],
    },
    upgrade_owner: {
      emoji:"📈", name:"현실 상향 보유자", tagline:"서울보다 같은 축의 상위 도시를 먼저 본다",
      quote:"무리하게 점프하기보다, 한 칸씩 올라가는 쪽이 맞아요.",
      tags:["보유자","갈아타기","상향 이동","가격 민감"],
    },
    lifestyle_dink: {
      emoji:"🎨", name:"지역내 감도 탐색 DINK", tagline:"서울 대신 지역 안에서 취향이 맞는 곳을 찾는다",
      quote:"꼭 중심부가 아니어도, 나답게 살 수 있는 동네는 있거든요.",
      tags:["DINK","라이프","취향","생활권 탐색"],
    },
    retiring_senior: {
      emoji:"🌾", name:"소형 정착 다운사이저", tagline:"관리 쉬운 집과 병원 접근성을 우선한다",
      quote:"이제는 넓은 집보다 편하게 사는 집이 먼저예요.",
      tags:["은퇴","시니어","다운사이징","의료 접근"],
    },
    return_local: {
      emoji:"↩️", name:"생활비 절감 복귀형", tagline:"지출을 줄이며 익숙한 지역으로 돌아온다",
      quote:"결국 버틸 수 있는 동네로 돌아오는 것도 전략이에요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"📉", name:"저가 진입 투자 청년", tagline:"작게 들어가서 사이클을 기다린다",
      quote:"비싼 데 못 들어가도, 아직 덜 오른 데는 남아 있어요.",
      tags:["청년","소액 투자","저가 진입","수익"],
    },
  },
  gyeonggi_rural: {
    steady_family: {
      emoji:"🚗", name:"차 중심 정착 가족", tagline:"학교와 병원을 차로 연결해 산다",
      quote:"도시는 멀어도, 아이 키우기만 괜찮으면 남을 수 있어요.",
      tags:["가족","실거주","학군","차 생활"],
    },
    upward_worker: {
      emoji:"🛣️", name:"광역 통근 직장인", tagline:"도로와 환승거점이 곧 집값이다",
      quote:"출퇴근이 길어도, 갈아탈 만한 다음 거점은 늘 찾게 돼요.",
      tags:["직장인","출퇴근","광역 통근","거점 이동"],
    },
    first_home: {
      emoji:"🔰", name:"정착형 첫 집 부부", tagline:"넓이와 비용을 먼저 보고 시작한다",
      quote:"서울은 아니어도, 첫 집은 오래 버틸 수 있어야 해요.",
      tags:["첫 매수","예산 제한","첫 집","정착"],
    },
    upgrade_owner: {
      emoji:"📦", name:"읍내 상향 보유자", tagline:"더 큰 도시보다 더 나은 거점을 찾는다",
      quote:"멀리 점프하기보다, 일단 더 나은 생활권으로 옮기고 싶어요.",
      tags:["보유자","갈아타기","상향 이동","생활권 이동"],
    },
    lifestyle_dink: {
      emoji:"🌲", name:"저밀도 취향 DINK", tagline:"복잡함보다 여유 있는 동네를 선호한다",
      quote:"도시의 속도보다, 내가 편한 리듬이 먼저예요.",
      tags:["DINK","라이프","저밀도","취향"],
    },
    retiring_senior: {
      emoji:"🪴", name:"전원형 다운사이저", tagline:"도시와 너무 멀지 않은 조용한 곳을 찾는다",
      quote:"의료 접근은 챙기되, 남은 시간은 조금 더 조용했으면 해요.",
      tags:["은퇴","시니어","다운사이징","노후"],
    },
    return_local: {
      emoji:"🏞️", name:"고향 복귀 정착자", tagline:"원래 살던 축으로 되돌아와 생활비를 맞춘다",
      quote:"결국 오래 버틸 수 있는 건 익숙한 생활권이더라고요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"📍", name:"거점 선점 투자 청년", tagline:"사람들이 뒤늦게 보는 생활거점을 먼저 찾는다",
      quote:"큰 상승장은 아니어도, 먼저 선점할 만한 자리들은 보여요.",
      tags:["청년","소액 투자","거점 선점","수익"],
    },
  },
  incheon_newtown: {
    steady_family: {
      emoji:"🌊", name:"신도시 실거주 가족", tagline:"학군과 바다 전망보다 생활 안정이 먼저다",
      quote:"새 동네의 장점은 누리되, 학교와 통근이 무너지면 안 돼요.",
      tags:["가족","실거주","학군","신도시"],
    },
    upward_worker: {
      emoji:"🚅", name:"서울 연결 직장인", tagline:"송도와 서울 사이 시간을 계속 계산한다",
      quote:"도시는 좋지만, 결국 서울과 얼마나 이어지느냐가 중요해요.",
      tags:["직장인","출퇴근","GTX","노선 민감"],
    },
    first_home: {
      emoji:"🏙️", name:"신도시 첫 집 부부", tagline:"서울 대신 쾌적함과 첫 자산을 고른다",
      quote:"첫 집이라면, 답답하지 않은 환경도 무시 못 하겠더라고요.",
      tags:["첫 매수","신혼","예산 제한","첫 집"],
    },
    upgrade_owner: {
      emoji:"📈", name:"해안권 상향 보유자", tagline:"송도 안에서 한 단계 위 단지를 본다",
      quote:"같은 도시 안에서도 단지마다 위계가 확실히 갈려요.",
      tags:["보유자","갈아타기","상향 이동","단지 위계"],
    },
    lifestyle_dink: {
      emoji:"🥂", name:"국제도시 DINK", tagline:"쾌적함과 도시 이미지를 함께 산다",
      quote:"서울처럼 빽빽하진 않아도, 나한텐 이 리듬이 더 맞아요.",
      tags:["DINK","라이프","도시 감","국제도시"],
    },
    retiring_senior: {
      emoji:"🌅", name:"해안형 다운사이저", tagline:"서울 대신 바다와 의료 접근성을 함께 본다",
      quote:"은퇴 후엔 바다를 가까이 두고 싶지만, 병원도 멀면 안 돼요.",
      tags:["은퇴","시니어","다운사이징","의료 접근"],
    },
    return_local: {
      emoji:"🧳", name:"인천 복귀 재정착자", tagline:"서울 생활을 정리하고 다시 인천으로 붙는다",
      quote:"서울에서 빠져나오더라도, 너무 낡은 생활권으로는 가고 싶지 않아요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"📡", name:"노선 기대 투자 청년", tagline:"GTX와 국제도시 스토리에 베팅한다",
      quote:"실현 전 기대감이 가격을 먼저 움직일 때가 있거든요.",
      tags:["청년","소액 투자","GTX","수익"],
    },
  },
  incheon_mixed: {
    steady_family: {
      emoji:"🏠", name:"구도심 정착 가족", tagline:"생활비와 학교를 맞추는 현실형 가족",
      quote:"새 아파트보다, 버틸 수 있는 생활비가 더 현실적이에요.",
      tags:["가족","실거주","학군","안정 선호"],
    },
    upward_worker: {
      emoji:"🚘", name:"서울 접근 직장인", tagline:"서울 출근선에 얼마나 붙을 수 있는지 본다",
      quote:"서울까지 한 시간 안에 들어가야 다음 집도 보이죠.",
      tags:["직장인","출퇴근","서울 접근","갈아타기"],
    },
    first_home: {
      emoji:"🔑", name:"실속 첫 매수층", tagline:"인천 안에서 가장 합리적인 첫 집을 찾는다",
      quote:"화려하진 않아도, 첫 집은 감당 가능한 쪽이 맞아요.",
      tags:["첫 매수","예산 제한","첫 집","실속"],
    },
    upgrade_owner: {
      emoji:"📦", name:"신도시 상향 보유자", tagline:"구도심에서 신도시나 서울 가까운 축을 본다",
      quote:"같은 인천이라도, 다음엔 좀 더 새 도시로 가고 싶어요.",
      tags:["보유자","갈아타기","상향 이동","신도시"],
    },
    lifestyle_dink: {
      emoji:"🍜", name:"동네 취향 DINK", tagline:"서울 대신 지역 안에서 분위기 맞는 곳을 찾는다",
      quote:"멀리 안 가도, 우리한테 맞는 생활권은 따로 있더라고요.",
      tags:["DINK","라이프","취향","생활권 탐색"],
    },
    retiring_senior: {
      emoji:"🌿", name:"생활비 절약 다운사이저", tagline:"현금흐름과 병원 거리를 같이 본다",
      quote:"노후엔 집의 상징성보다, 병원과 생활비가 더 크게 보여요.",
      tags:["은퇴","시니어","다운사이징","생활비 절감"],
    },
    return_local: {
      emoji:"↩️", name:"인천 회귀형", tagline:"비용을 줄이며 익숙한 생활권으로 돌아온다",
      quote:"결국 손에 익은 도시로 돌아오는 것도 나쁜 선택은 아니에요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"🎯", name:"저평가 베팅 청년", tagline:"송도 옆 구도심의 간극을 본다",
      quote:"사람들이 덜 보는 곳에서 차이를 먹는 게 제 전략이에요.",
      tags:["청년","소액 투자","저평가","수익"],
    },
  },
  incheon_island: {
    steady_family: {
      emoji:"⛴️", name:"섬-본토 왕복 가족", tagline:"배와 차 시간표까지 계산해 산다",
      quote:"학교와 병원이 멀수록, 이사는 더 신중해질 수밖에 없어요.",
      tags:["가족","실거주","학군","왕복 생활"],
    },
    upward_worker: {
      emoji:"🧭", name:"본토 거점 직장인", tagline:"섬을 떠나 본토 거점으로 붙고 싶다",
      quote:"결국 다음 집은 인천 본토나 서울 접근선일 가능성이 커요.",
      tags:["직장인","출퇴근","거점 이동","복귀"],
    },
    first_home: {
      emoji:"🪵", name:"저비용 첫 정착층", tagline:"비용을 아끼되 생활 가능선을 본다",
      quote:"싸다는 이유만으론 안 되고, 버틸 수 있는 생활권이어야 해요.",
      tags:["첫 매수","예산 제한","정착","실속"],
    },
    upgrade_owner: {
      emoji:"🧱", name:"본토 상향 보유자", tagline:"섬 안이 아니라 본토 쪽 다음 집을 생각한다",
      quote:"다음 집은 같은 섬이 아니라, 연결이 더 나은 쪽이어야 해요.",
      tags:["보유자","갈아타기","상향 이동","본토 이동"],
    },
    lifestyle_dink: {
      emoji:"🌊", name:"저밀도 취향 DINK", tagline:"도시 속도보다 풍경과 밀도를 우선한다",
      quote:"불편함이 있어도, 이 풍경과 간격은 다른 데서 못 구해요.",
      tags:["DINK","라이프","저밀도","취향"],
    },
    retiring_senior: {
      emoji:"🪁", name:"조용한 노후 지향층", tagline:"병원은 가깝게, 생활 속도는 느리게 가져간다",
      quote:"고요함은 좋지만, 의료와 장보기는 계속 고민이에요.",
      tags:["은퇴","시니어","다운사이징","노후"],
    },
    return_local: {
      emoji:"🏝️", name:"섬 복귀 정착자", tagline:"도시를 떠나 익숙한 생활권으로 다시 온다",
      quote:"결국 불편해도 익숙한 생활권으로 돌아오는 사람도 있어요.",
      tags:["귀향","재정착","복귀","생활비 절감"],
    },
    young_investor: {
      emoji:"📍", name:"거점 탐색 투자 청년", tagline:"섬보단 연결되는 본토 거점을 노린다",
      quote:"여기는 실거주보다도, 다음에 어디로 붙을지가 더 중요해요.",
      tags:["청년","소액 투자","거점 이동","수익"],
    },
  },
};

function buildGenericPersonas(region) {
  var profile = genericPersonaProfile(region);
  var variant = GENERIC_PERSONA_VARIANTS[profile] || GENERIC_PERSONA_VARIANTS.gyeonggi_commuter;
  return PERSONAS.__generic.map(function(base){
    var o = variant[base.id] || {};
    return {
      id: base.id,
      emoji: o.emoji || base.emoji,
      name: o.name || base.name,
      tagline: o.tagline || base.tagline,
      quote: o.quote || base.quote,
      tags: o.tags || base.tags,
    };
  });
}

/* ============================================================
   REPORTS — distinct destinations + narratives per (region, persona)
   Helper to factor out boilerplate.
   ============================================================ */
function dest(role, name, category, score, desc, metrics, narrative, extra) {
  const labels = {
    peer: { roleLabel:"PEER VIEW", roleKr:"현실 후보" },
    up:   { roleLabel:"UPWARD STEP", roleKr:"갈 법한 상향지" },
    wish: { roleLabel:"WISH REACHABLE", roleKr:"가보고 싶은 곳" },
    wishcore: { roleLabel:"WISH CORE", roleKr:"핵심 욕망" },
  };
  return { id: name, role, ...labels[role], name, category, score, desc, metrics, narrative, ...(extra || {}) };
}

const REPORTS = {
  /* ─────────────── 마포 ─────────────── */
  mapo: {
    publisher: { destinations: [
      dest("peer","합정","현재 동네 안의 다음 자리",78,
        "지금의 생활 반경을 거의 그대로 유지하면서 한 칸 위로 올라가는 가장 현실적인 선택지.",
        { desire:64, mobility:72, policy:81, priceGap:{value:"+1.4억",pct:11,dir:"up"}, pricePsm:"5,820만/㎡", ltv:"안정", timeline:"6–12개월", commute:"−4분" },
        { title:"<em>익숙함</em>의 이름으로 한 칸 더",
          paragraphs:[
            "<span class='hl'>합정은 마포구 안에서의 미세한 상승</span>이다. 직장과 동선, 금요일 밤의 술집, 토요일 아침의 빵집까지 모두 유지된다. 페르소나의 의사결정에서 리스크가 가장 낮은 후보.",
            "다만 합정의 신축 평형은 마포 중심부 구축과 가격 분리가 시작되는 지점이다. 1.4억의 격차는 LTV 한도 안에서 흡수 가능하지만, 자녀 학령기 진입과 동시에 결심해야 한다." ],
          pull:{ q:"한 정거장만 옮기는 일이, 가장 어려운 일일 수도 있다.", cite:"에디터 노트 · 합정" } }),
      dest("up","성수","동시대 자산의 다음 챕터",86,
        "마포의 미디어 DNA가 자연스럽게 다음 단계로 흘러가는 자리. 한강을 건너지 않고도 도시의 격을 한 단계 올린다.",
        { desire:88, mobility:54, policy:62, priceGap:{value:"+5.2억",pct:38,dir:"up"}, pricePsm:"7,940만/㎡", ltv:"압박", timeline:"18–36개월", commute:"+11분" },
        { title:"<em>가능성</em>의 이름으로 강을 따라 동쪽",
          paragraphs:[
            "성수는 마포 페르소나가 가장 자주 떠올리는 단어다. 같은 한강 라인, 같은 크리에이티브 산업, 그러나 <span class='hl'>한 단계 위의 가격대와 한 단계 위의 사회적 신호</span>.",
            "이동가능성 점수는 중간(54). 자기자본 격차는 분명하지만, 24–36개월 내 자산 재편 시 가장 자연스러운 동선." ],
          pull:{ q:"성수는 마포의 다음 페이지처럼 읽힌다.", cite:"에디터 노트 · 성수" } }),
      dest("wish","한남","끝까지 가보고 싶은 동네",94,
        "지금 당장의 현실적 선택은 아니지만, 의사결정의 가장 깊은 곳에서 작동하는 좌표. 가격이 아니라 정체성이 기준이 되는 자리.",
        { desire:97, mobility:22, policy:41, priceGap:{value:"+18.6억",pct:135,dir:"up"}, pricePsm:"1.34억/㎡", ltv:"불가", timeline:"60개월+", commute:"+18분" },
        { title:"<em>끝까지 가보고 싶은</em> 동네",
          paragraphs:[
            "한남은 자산 데이터에서 가장 멀리 있는 점이지만, 욕망 점수에서는 가장 가깝다. <span class='hl'>가격이 아니라 정체성이 기준이 되는 자리</span>.",
            "이동가능성 22점. 단순 매수 시나리오로는 도달이 어렵다. 다만 다른 두 후보를 평가할 때 보이지 않는 기준점으로 작동한다. 한남은 결과가 아니라 좌표다." ],
          pull:{ q:"갈 수 없는 동네가, 갈 수 있는 동네의 가격을 결정한다.", cite:"에디터 노트 · 한남" } }),
    ]},
  },

  /* ─────────────── 송파 ─────────────── */
  songpa: {
    "jamsil-mom": { destinations: [
      dest("peer","문정","학원 동선을 깨지 않는 한 칸",74,
        "잠실 학원가를 그대로 유지하면서 단지 평수만 키울 수 있는 자리. 같은 송파 안에서 움직이는 가장 보수적인 선택.",
        { desire:62, mobility:78, policy:74, priceGap:{value:"+0.9억",pct:6,dir:"up"}, pricePsm:"6,100만/㎡", ltv:"안정", timeline:"6–9개월", commute:"−2분" },
        { title:"<em>학원 동선</em>은 깨지 않는다",
          paragraphs:[
            "<span class='hl'>잠실에서 문정으로의 이동은 송파 안에서의 미세 조정</span>이다. 학원가는 그대로, 단지 노후도와 평수만 다음 단계로 옮긴다.",
            "송파 안에서 가족 단위 매수자가 가장 합리적으로 선택하는 경로. 자녀 학령기와 매수 사이클이 일치할 때 실행 확률이 가장 높다." ],
          pull:{ q:"학원가 반경을 유지한 채로 평수만 늘린다.", cite:"에디터 노트 · 문정" } }),
      dest("up","대치","학군의 본진으로 건너가는 일",89,
        "송파 학부모가 가장 자주 호명하는 다음 좌표. 같은 자녀를 두고도 학군의 격이 한 단계 올라가는 자리.",
        { desire:91, mobility:48, policy:53, priceGap:{value:"+8.4억",pct:42,dir:"up"}, pricePsm:"9,180만/㎡", ltv:"압박", timeline:"18–30개월", commute:"+7분" },
        { title:"<em>학군의 본진</em>으로",
          paragraphs:[
            "송파 학부모에게 대치는 가까운 듯 멀다. 한 정거장 차이지만 <span class='hl'>학군의 위계와 가격의 위계가 동시에 다른</span> 동네.",
            "이동가능성 48점. 자녀 입시 사이클과 학군 진입 타이밍이 맞물려야 실행된다. 잘못된 타이밍의 매수는 자녀에게도 가족에게도 손해." ],
          pull:{ q:"같은 자녀에게 다른 학군을 주는 일.", cite:"에디터 노트 · 대치" } }),
      dest("wish","압구정","마지막으로 한 번 더 올라간다",95,
        "학군과 자산의 마지막 페이지. 송파 학부모의 의사결정 깊은 곳에 놓인 좌표.",
        { desire:96, mobility:18, policy:36, priceGap:{value:"+22.3억",pct:138,dir:"up"}, pricePsm:"1.52억/㎡", ltv:"불가", timeline:"60개월+", commute:"+12분" },
        { title:"<em>마지막 한 번</em>의 매수",
          paragraphs:[
            "압구정은 송파 학부모의 의사결정 가장 깊은 곳에 놓인 좌표. <span class='hl'>학군이 끝난 뒤에도 자산으로 남는 자리</span>.",
            "이동가능성 18점. 단순 매수로는 불가. 자산 정리·증여·재건축 사이클 모두를 고려해야 도달 가능한 지점." ],
          pull:{ q:"학군이 끝나도 남는 자리가, 자산이 된다.", cite:"에디터 노트 · 압구정" } }),
    ]},
  },

  /* ─────────────── 성동 ─────────────── */
  seongdong: {
    "seongsu-pm": { destinations: [
      dest("peer","왕십리","일과 거주의 균형이 유지되는 자리",76,
        "성수 사옥 출퇴근을 그대로 유지하면서 가격 부담만 한 단계 낮추는 동선. 성동 안에서 움직이는 가장 안정적인 후보.",
        { desire:60, mobility:81, policy:78, priceGap:{value:"−0.6억",pct:-4,dir:"down"}, pricePsm:"6,420만/㎡", ltv:"여유", timeline:"3–9개월", commute:"+3분" },
        { title:"<em>도시의 코어</em>로 한 발",
          paragraphs:[
            "왕십리는 성수의 사옥 라인을 유지하면서 가격은 살짝 내려가는 자리. <span class='hl'>일과 집의 거리가 거의 그대로</span>인 채로 자산 구조만 바꾸는 후보.",
            "성수 거주자가 가장 자주 검토하는 백업 옵션. 직주근접을 잃지 않는 선에서 자기자본의 여유를 만든다." ],
          pull:{ q:"움직이는 게 아니라, 다시 자리를 잡는 일.", cite:"에디터 노트 · 왕십리" } }),
      dest("up","한남","강 건너로의 한 칸",90,
        "성수에서 한강을 건너 강남쪽 동선을 잡는 가장 자연스러운 다음 페이지.",
        { desire:92, mobility:38, policy:48, priceGap:{value:"+11.8억",pct:78,dir:"up"}, pricePsm:"1.18억/㎡", ltv:"압박", timeline:"30–48개월", commute:"+9분" },
        { title:"<em>강 건너의 거실</em>",
          paragraphs:[
            "성수 페르소나에게 한남은 가장 자주 호명되는 다음 좌표. <span class='hl'>같은 한강 라인이지만 강의 다른 쪽</span>이라는 사실이 만드는 사회적 격차.",
            "이동가능성 38점. 자기자본 격차가 크고, 사옥과 거주의 분리가 본격화되는 시점." ],
          pull:{ q:"한강을 매일 건너는 일이 곧 정체성이 된다.", cite:"에디터 노트 · 한남" } }),
      dest("wish","청담","끝까지 가본다면",94,
        "성수의 라이프스타일을 가장 비슷한 밀도로 받아주면서 가격은 가장 멀리 있는 좌표.",
        { desire:95, mobility:20, policy:34, priceGap:{value:"+19.2억",pct:128,dir:"up"}, pricePsm:"1.46억/㎡", ltv:"불가", timeline:"60개월+", commute:"+13분" },
        { title:"<em>가장 비슷한 밀도</em>의 가장 먼 자리",
          paragraphs:[
            "청담은 성수의 카페·갤러리·패션 밀도를 거의 그대로 받지만, <span class='hl'>가격은 한참 떨어져 있다</span>. 라이프스타일은 같지만 자산은 다르다.",
            "이동가능성 20점. 매수 시나리오는 거의 불가에 가깝고, 욕망의 좌표로 더 자주 작동한다." ],
          pull:{ q:"비슷한 풍경의 가장 먼 가격.", cite:"에디터 노트 · 청담" } }),
    ]},
  },

  /* ─────────────── 강동 ─────────────── */
  gangdong: {
    "godeok-newbuild": { destinations: [
      dest("peer","둔촌","같은 권역의 신축 두 번째",80,
        "고덕에서 둔촌(올림픽파크포레온)으로의 이동은 같은 강동권 안의 신축 갈아타기. 9호선 동선을 유지하면서 단지의 격만 한 단계 올린다.",
        { desire:72, mobility:74, policy:71, priceGap:{value:"+2.8억",pct:18,dir:"up"}, pricePsm:"5,640만/㎡", ltv:"안정", timeline:"9–18개월", commute:"−2분" },
        { title:"<em>신축 다음의 신축</em>",
          paragraphs:[
            "고덕 신축의 단맛을 본 가족에게 둔촌은 자연스러운 다음 페이지. <span class='hl'>같은 권역, 같은 9호선, 그러나 다른 단지의 무게</span>.",
            "강동 안에서 일어나는 가장 빈번한 갈아타기 시나리오. 자녀 학령기와 사이클이 맞물릴 때 실행 확률이 가장 높다." ],
          pull:{ q:"신축은 한 번 살면 다시 못 돌아간다.", cite:"에디터 노트 · 둔촌" } }),
      dest("up","송파(헬리오시티)","대단지의 다음 위계로",84,
        "강동의 신축 가족이 가장 자주 그리는 다음 자리. 9호선 라인을 그대로 두고 송파권으로 한 단계 올라간다.",
        { desire:86, mobility:52, policy:58, priceGap:{value:"+6.4억",pct:38,dir:"up"}, pricePsm:"7,180만/㎡", ltv:"압박", timeline:"24–36개월", commute:"+5분" },
        { title:"<em>송파의 무게</em>를 시도한다",
          paragraphs:[
            "강동 신축 가족에게 헬리오시티는 같은 노선 위의 다른 위계. <span class='hl'>같은 9호선이지만 학군과 인프라의 무게가 다르다</span>.",
            "이동가능성 52점. 24–36개월 내 자산 재편이 가능한 가족에게 가장 현실적인 상향 후보." ],
          pull:{ q:"같은 노선 위에 다른 위계가 있다.", cite:"에디터 노트 · 헬리오시티" } }),
      dest("wish","잠실(엘리트)","다음 사이클의 마지막 매수",92,
        "강동 신축 가족의 의사결정 가장 깊은 곳에 놓인 좌표. 학군과 자산의 마지막 매수.",
        { desire:93, mobility:24, policy:42, priceGap:{value:"+14.6억",pct:88,dir:"up"}, pricePsm:"1.04억/㎡", ltv:"불가", timeline:"48개월+", commute:"+8분" },
        { title:"<em>마지막 매수</em>로서의 잠실",
          paragraphs:[
            "잠실 엘·리·트는 강동 가족이 다음 사이클의 마지막 매수 후보로 자주 호명한다. <span class='hl'>학군과 자산이 동시에 끝나는 자리</span>.",
            "이동가능성 24점. 자기자본만으로는 어렵고, 강동 자산 전부를 정리하는 시나리오에서 가능." ],
          pull:{ q:"한 번 더 위로, 그리고 마지막으로.", cite:"에디터 노트 · 잠실" } }),
    ]},
  },

  /* ─────────────── 분당 ─────────────── */
  "gyeonggi-31023": {
    "pangyo-eng": { destinations: [
      dest("peer","서현","분당 안의 미세 조정",75,
        "정자에서 서현으로의 이동은 분당 안에서의 작은 상향. 학원가와 판교 출근 동선을 모두 유지한다.",
        { desire:64, mobility:80, policy:73, priceGap:{value:"+1.1억",pct:9,dir:"up"}, pricePsm:"5,140만/㎡", ltv:"안정", timeline:"6–12개월", commute:"+2분" },
        { title:"<em>분당 안</em>에서의 한 칸",
          paragraphs:[
            "서현은 판교 통근과 학원가를 유지하면서 단지 노후도와 평수만 한 단계 옮기는 자리. <span class='hl'>분당 페르소나의 가장 보수적인 갈아타기</span>.",
            "재건축 사이클이 본격화되기 전, 신축 비중이 높은 단지로 미리 자리를 잡는 시나리오." ],
          pull:{ q:"분당을 떠나지 않고도 한 칸 위로.", cite:"에디터 노트 · 서현" } }),
      dest("up","판교 백현","테크의 본진으로",87,
        "판교역 도보권. 회사와 집의 거리를 분 단위로 줄이는 자리.",
        { desire:90, mobility:46, policy:58, priceGap:{value:"+6.8억",pct:42,dir:"up"}, pricePsm:"8,940만/㎡", ltv:"압박", timeline:"18–30개월", commute:"−18분" },
        { title:"<em>회사 옆에 산다</em>",
          paragraphs:[
            "백현은 판교 엔지니어가 가장 자주 호명하는 다음 좌표. <span class='hl'>회사와 집의 거리가 분 단위로 좁혀지는</span> 동네.",
            "자기자본 격차는 크지만, 통근 비용과 시간 가치를 함께 계산하면 실행 가능 구간." ],
          pull:{ q:"통근이 없는 도시에 들어간다.", cite:"에디터 노트 · 백현" } }),
      dest("wish","압구정·도곡","강남으로의 도약",93,
        "분당 페르소나가 마지막에 그리는 좌표. 가격이 아니라 격이 기준이 되는 자리.",
        { desire:94, mobility:22, policy:38, priceGap:{value:"+17.4억",pct:108,dir:"up"}, pricePsm:"1.48억/㎡", ltv:"불가", timeline:"60개월+", commute:"+22분" },
        { title:"<em>강남으로의 도약</em>",
          paragraphs:[
            "분당이 끝나는 자리에서 강남이 시작된다. <span class='hl'>분당의 가족형 안정감이 끝나고, 도곡·압구정의 학군과 자산이 시작되는 좌표</span>.",
            "이동가능성 22점. 자녀 학령기와 자산 정리 사이클이 동시에 맞아야 가능." ],
          pull:{ q:"가격이 아니라 격이 기준이 되는 자리.", cite:"에디터 노트 · 강남" } }),
    ]},
  },

  /* ─────────────── 영통(광교) ─────────────── */
  "gyeonggi-31014": {
    "samsung-eng": { destinations: [
      dest("peer","광교중앙","호수공원 옆 신축",74,
        "매탄에서 광교중앙으로의 이동. 회사 통근 동선은 그대로, 단지의 격만 한 단계 위로.",
        { desire:66, mobility:79, policy:75, priceGap:{value:"+0.8억",pct:9,dir:"up"}, pricePsm:"3,940만/㎡", ltv:"여유", timeline:"6–12개월", commute:"+1분" },
        { title:"<em>호수공원 옆</em>의 신축",
          paragraphs:[
            "광교중앙은 영통 페르소나의 가장 빈번한 다음 자리. <span class='hl'>같은 통근, 같은 학군, 새로운 단지</span>.",
            "삼성 R&D 직장인의 매수 패턴 중 가장 빈도가 높은 시나리오. 자기자본 부담이 가장 작다." ],
          pull:{ q:"통근을 바꾸지 않고 단지를 바꾼다.", cite:"에디터 노트 · 광교중앙" } }),
      dest("up","분당 정자","한 칸 위의 도시로",83,
        "영통에서 분당 정자로의 이동. 같은 경부 라인이지만 도시의 위계가 한 단계 다르다.",
        { desire:84, mobility:50, policy:60, priceGap:{value:"+5.6억",pct:62,dir:"up"}, pricePsm:"6,720만/㎡", ltv:"압박", timeline:"24–36개월", commute:"+11분" },
        { title:"<em>분당 위계</em>로의 진입",
          paragraphs:[
            "정자는 영통 페르소나가 가장 자주 그리는 상향 좌표. <span class='hl'>같은 경부 라인, 다른 도시 위계</span>.",
            "삼성 R&D 출퇴근을 유지하면서도 분당 학군과 인프라로 진입하는 가장 자연스러운 동선." ],
          pull:{ q:"같은 라인 위에 다른 도시가 있다.", cite:"에디터 노트 · 정자" } }),
      dest("wish","서초·반포","경부 라인의 끝",91,
        "영통 페르소나의 욕망 좌표. 자녀 입시 사이클과 함께 가장 자주 호명되는 종착지.",
        { desire:93, mobility:18, policy:36, priceGap:{value:"+22.8억",pct:198,dir:"up"}, pricePsm:"1.62억/㎡", ltv:"불가", timeline:"60개월+", commute:"+34분" },
        { title:"<em>경부 라인의 끝</em>",
          paragraphs:[
            "반포는 영통 페르소나에게 같은 노선의 끝점이지만 가격은 다른 차원. <span class='hl'>학군과 자산의 마지막 페이지</span>.",
            "이동가능성 18점. 자산 정리·증여·재건축 사이클을 모두 고려해야 가능." ],
          pull:{ q:"같은 노선의 끝, 다른 차원의 가격.", cite:"에디터 노트 · 반포" } }),
    ]},
  },

  /* ─────────────── 덕양(GTX-A·창릉) ─────────────── */
  "gyeonggi-31101": {
    "gtx-commuter": { destinations: [
      dest("peer","원흥·삼송","GTX-A 효과의 다음 단지",73,
        "덕양 안에서 GTX-A 노선 효과를 유지하면서 단지만 한 단계 신축으로 옮긴다.",
        { desire:62, mobility:80, policy:76, priceGap:{value:"+0.5억",pct:7,dir:"up"}, pricePsm:"3,280만/㎡", ltv:"여유", timeline:"6–12개월", commute:"+1분" },
        { title:"<em>같은 노선</em>의 다음 단지",
          paragraphs:[
            "삼송·원흥은 GTX-A 통근자가 가장 자주 검토하는 다음 자리. <span class='hl'>노선은 그대로, 단지만 신축</span>.",
            "GTX-A 노선이 본격 운행되며 가격 분화가 진행 중. 24개월 내 매수 시 자산 가치 재평가 가능성." ],
          pull:{ q:"같은 노선이 시간을 두 번 만든다.", cite:"에디터 노트 · 삼송" } }),
      dest("up","마곡","GTX의 끝, 강서의 시작",81,
        "GTX-A 종점이자 9호선·5호선이 만나는 자리. 서울 입성을 가장 합리적으로 만드는 동선.",
        { desire:84, mobility:54, policy:62, priceGap:{value:"+4.2억",pct:54,dir:"up"}, pricePsm:"5,120만/㎡", ltv:"압박", timeline:"24–36개월", commute:"−12분" },
        { title:"<em>서울 입성</em>의 합리적 동선",
          paragraphs:[
            "마곡은 GTX-A 통근자가 자녀 입학 시점에 가장 자주 호명하는 다음 자리. <span class='hl'>경기에서 서울로 건너가는 가장 합리적인 가격대</span>.",
            "자기자본 격차는 분명하지만, 자녀 학령기와 GTX 정착 효과가 맞물리면 실행 확률이 급격히 올라간다." ],
          pull:{ q:"경기에서 서울로, 가장 짧은 점프.", cite:"에디터 노트 · 마곡" } }),
      dest("wish","상암·DMC","미디어 도시의 자리",88,
        "덕양 페르소나가 한강 라인 안쪽에서 가장 자주 그리는 욕망 좌표. 한강을 가까이 둔 서울 서북부의 다음 자리.",
        { desire:90, mobility:30, policy:48, priceGap:{value:"+8.6억",pct:112,dir:"up"}, pricePsm:"7,820만/㎡", ltv:"불가", timeline:"48개월+", commute:"−8분" },
        { title:"<em>한강 안쪽</em>으로",
          paragraphs:[
            "상암은 덕양 페르소나에게 한강 라인 안쪽의 자리. <span class='hl'>같은 서북부지만 한강을 끼고 있다는 사실</span>이 만드는 위계 차이.",
            "이동가능성 30점. 자기자본 격차와 자녀 학령기, 두 변수가 동시에 풀려야 가능한 좌표." ],
          pull:{ q:"같은 서북부, 다른 한강.", cite:"에디터 노트 · 상암" } }),
    ]},
  },

  /* ─────────────── 인천 연수(송도) ─────────────── */
  "incheon-23040": {
    "songdo-bio": { destinations: [
      dest("peer","송도 6공구","바이오 본진의 신축",72,
        "송도 안에서의 미세 조정. 같은 회사 통근, 새로운 단지.",
        { desire:60, mobility:82, policy:78, priceGap:{value:"+0.6억",pct:9,dir:"up"}, pricePsm:"3,180만/㎡", ltv:"여유", timeline:"3–9개월", commute:"+0분" },
        { title:"<em>같은 도시</em>의 새 단지",
          paragraphs:[
            "송도 6공구는 송도 거주자의 가장 자연스러운 다음 자리. <span class='hl'>회사·학교·생활권 모두 그대로</span>, 단지의 격만 한 단계 위.",
            "송도 안에서의 갈아타기는 자기자본 부담이 가장 낮고, 송도 외부로 나가지 않으려는 페르소나의 강한 선호와 일치한다." ],
          pull:{ q:"송도 안에 있어야 송도가 산다.", cite:"에디터 노트 · 6공구" } }),
      dest("up","마곡","서울 입성, 가장 짧은 거리",82,
        "GTX-B의 미래와 9호선의 현재가 만나는 자리. 송도 페르소나의 가장 빈번한 서울 입성 후보.",
        { desire:85, mobility:48, policy:56, priceGap:{value:"+5.4억",pct:78,dir:"up"}, pricePsm:"5,120만/㎡", ltv:"압박", timeline:"30–48개월", commute:"−24분" },
        { title:"<em>서울 입성</em>의 첫 자리",
          paragraphs:[
            "마곡은 송도 페르소나가 서울로 건너오는 첫 자리로 자주 호명한다. <span class='hl'>같은 9호선, 같은 직주근접 구조</span>를 받으면서 도시의 격은 다르다.",
            "이동가능성 48점. GTX-B 정착 시점과 자녀 학령기가 동시에 풀려야 실행." ],
          pull:{ q:"송도가 끝나는 자리에서 서울이 시작된다.", cite:"에디터 노트 · 마곡" } }),
      dest("wish","반포·잠원","끝까지 간다면",90,
        "송도 페르소나의 욕망 좌표. 같은 한강이지만 한참 안쪽으로 들어간 자리.",
        { desire:92, mobility:14, policy:32, priceGap:{value:"+24.6억",pct:286,dir:"up"}, pricePsm:"1.62억/㎡", ltv:"불가", timeline:"60개월+", commute:"+38분" },
        { title:"<em>같은 한강</em>의 다른 좌표",
          paragraphs:[
            "반포는 송도 페르소나에게 같은 한강이지만 다른 좌표. <span class='hl'>송도가 만드는 안정과 반포가 만드는 위계는 다른 차원의 자산</span>.",
            "이동가능성 14점. 자산 정리만으로는 어렵고, 자녀 입시·증여·자산 재편이 모두 동시에 작동해야 가능." ],
          pull:{ q:"같은 강, 다른 차원의 가격.", cite:"에디터 노트 · 반포" } }),
    ]},
  },
};

/* Generic builder: persona-trait-aware destination selection.
   Reads persona tags/tagline to detect school, commute, invest, downsize,
   lifestyle, first-buy signals and weights candidates accordingly. */
function buildGenericReport(region, persona) {
  const peerPulls = [
    "가까운 곳에서 시작하는 게, 가장 멀리 가는 길이다.",
    "한 정거장만 옮기는 일이, 가장 어려운 일일 수도 있다.",
    "같은 동네에서의 갈아타기가 가장 정직한 매수다.",
    "익숙함의 이름으로 한 칸 더.",
    "통근을 바꾸지 않고 단지를 바꾼다.",
    "가까운 자리는, 가장 늦게 보인다.",
  ];
  const upPulls = [
    "같은 노선에 다른 위계가 있다.",
    "도시의 격이 한 단계 바뀌는 자리.",
    "다음 챕터는, 늘 한 정거장 안쪽에 있다.",
    "같은 라인 위에 다른 도시가 있다.",
    "한 칸 위로 올라가는 일은, 한 칸의 일이 아니다.",
    "가까운 격차가 가장 비싼 격차다.",
  ];
  const wishPulls = [
    "갈 수 없는 동네가, 갈 수 있는 동네의 가격을 결정한다.",
    "가격이 아니라 정체성이 기준이 되는 자리.",
    "도달하지 않은 좌표가, 모든 의사결정을 비춘다.",
    "닿지 않는 동네의 이름이, 결정의 기준이 된다.",
    "끝까지 가본다면, 거기에서 다시 시작된다.",
    "같은 강, 다른 차원의 가격.",
  ];
  var seed = (region.id + ":" + persona.id).split("").reduce(function(a,c){return a*31 + c.charCodeAt(0)|0}, 0);
  var pick = function(arr, off) { return arr[Math.abs(seed + off) % arr.length]; };

  // ── 1. Persona trait detection ──
  var sig = [persona.name, persona.tagline].concat(persona.tags || []).join(" ");
  var is_ = {
    school:   /학군|학원|자녀|학부모|아이|교육|초등|손주|돌봄/.test(sig),
    commute:  /직장인|출퇴근|통근|사옥|회사|직주|교대|출근|근접|산단|연구|R&D/.test(sig),
    invest:   /투자|갭투자|다주택|재건축|수익|사이클|소액/.test(sig),
    upgrade:  /갈아타기|보유자|상향 이동|위계|다음 집/.test(sig),
    downsize: /은퇴|다운|연금|소형|독거|시니어|노후/.test(sig),
    lifestyle:/DINK|취향|라이프|프리랜|카페|아티스트|도시 감|반려/.test(sig),
    firstBuy: /첫 매수|첫매수|예산|청년|신혼|첫 집/.test(sig),
    returnLocal: /귀향|재정착|복귀|돌아온|돌아오/.test(sig),
  };

  var wishTrait = is_.school ? "school"
    : is_.invest ? "invest"
    : is_.lifestyle ? "lifestyle"
    : is_.commute ? "commute"
    : is_.returnLocal ? "returnLocal"
    : is_.upgrade ? "upgrade"
    : is_.downsize ? "downsize"
    : is_.firstBuy ? "firstBuy"
    : "default";

  // ── 2. Area trait magnets (regex on id + name) ──
  var SCHOOL   = /gangnam|seocho|songpa|yangcheon|nowon|gyeonggi-31023|대치|목동|중계|분당|수내|잠실|서초|반포|도곡|학군/;
  var COMMUTE  = /gangnam|seocho|yeongdeungpo|mapo|seongdong|guro|^jung$|jongno|강남|여의도|판교|성수|마곡|구로디지털|테헤란/;
  var LIFESTYLE= /seongdong|mapo|yongsan|성수|합정|연남|한남|이태원|용산|청담|망원|홍대/;
  var INVEST   = /gangdong|둔촌|고덕|동탄|송도|광교|위례|창릉|신도시|3기/;

  var MICRO_WISH_CANDIDATES = [
    { id:"micro-hannam", name:"한남동", shortName:"한남동", parent:"yongsan", parentRegion:"seoul", price:27.5, dx:8, dy:-8,
      magnets:{ lifestyle:9, commute:5, school:3, invest:3, upgrade:4, default:4 } },
    { id:"micro-cheongdam", name:"청담동", shortName:"청담동", parent:"gangnam", parentRegion:"seoul", price:29.8, dx:20, dy:6,
      magnets:{ lifestyle:9, commute:6, school:3, invest:4, upgrade:5, default:4 } },
    { id:"micro-apgujeong", name:"압구정동", shortName:"압구정동", parent:"gangnam", parentRegion:"seoul", price:31.2, dx:14, dy:-4,
      magnets:{ lifestyle:6, commute:4, school:7, invest:5, upgrade:6, default:4 } },
    { id:"micro-daechi", name:"대치동", shortName:"대치동", parent:"gangnam", parentRegion:"seoul", price:28.4, dx:10, dy:16,
      magnets:{ school:10, commute:5, invest:3, upgrade:5, default:3 } },
    { id:"micro-banpo", name:"반포동", shortName:"반포동", parent:"seocho", parentRegion:"seoul", price:30.6, dx:8, dy:8,
      magnets:{ school:9, commute:5, invest:4, upgrade:6, default:4 } },
    { id:"micro-jamwon", name:"잠원동", shortName:"잠원동", parent:"seocho", parentRegion:"seoul", price:27.8, dx:4, dy:-6,
      magnets:{ school:7, lifestyle:4, commute:4, upgrade:5, default:3 } },
    { id:"micro-banpojamwon", name:"반포·잠원", shortName:"반포·잠원", parent:"seocho", parentRegion:"seoul", price:29.4, dx:6, dy:2,
      magnets:{ school:8, lifestyle:4, commute:4, upgrade:6, default:4 } },
    { id:"micro-seongsu", name:"성수동", shortName:"성수동", parent:"seongdong", parentRegion:"seoul", price:20.2, dx:10, dy:2,
      magnets:{ lifestyle:10, commute:7, upgrade:5, default:4 } },
    { id:"micro-hapjeong", name:"합정동", shortName:"합정동", parent:"mapo", parentRegion:"seoul", price:18.6, dx:-8, dy:0,
      magnets:{ lifestyle:9, commute:6, upgrade:4, default:4 } },
    { id:"micro-yeouido", name:"여의도동", shortName:"여의도동", parent:"yeongdeungpo", parentRegion:"seoul", price:21.8, dx:-2, dy:-10,
      magnets:{ commute:10, invest:4, upgrade:4, default:4 } },
    { id:"micro-mokdong", name:"목동", shortName:"목동", parent:"yangcheon", parentRegion:"seoul", price:17.2, dx:-12, dy:-4,
      magnets:{ school:10, commute:3, downsize:3, default:3 } },
    { id:"micro-junggye", name:"중계동", shortName:"중계동", parent:"nowon", parentRegion:"seoul", price:13.2, dx:10, dy:-10,
      magnets:{ school:8, default:2 } },
    { id:"micro-jeongja", name:"정자동", shortName:"정자동", parent:"gyeonggi-31023", parentRegion:"gyeonggi", price:18.4, dx:0, dy:-8,
      magnets:{ commute:6, school:7, upgrade:7, downsize:5, returnLocal:4, default:4 } },
    { id:"micro-seohyeon", name:"서현동", shortName:"서현동", parent:"gyeonggi-31023", parentRegion:"gyeonggi", price:16.7, dx:8, dy:6,
      magnets:{ school:8, commute:4, upgrade:5, default:4 } },
    { id:"micro-baekhyeon", name:"백현동", shortName:"백현동", parent:"gyeonggi-31023", parentRegion:"gyeonggi", price:20.5, dx:-8, dy:-2,
      magnets:{ commute:7, lifestyle:4, upgrade:6, default:4 } },
    { id:"micro-pangyo", name:"판교동", shortName:"판교동", parent:"gyeonggi-31023", parentRegion:"gyeonggi", price:19.4, dx:-12, dy:10,
      magnets:{ commute:8, school:6, upgrade:6, invest:3, default:4 } },
    { id:"micro-gwanggyo", name:"광교중앙", shortName:"광교중앙", parent:"gyeonggi-31014", parentRegion:"gyeonggi", price:10.1, dx:10, dy:-4,
      magnets:{ school:6, commute:5, invest:4, upgrade:5, downsize:4, returnLocal:4, default:4 } },
    { id:"micro-songdo", name:"송도국제도시", shortName:"송도국제도시", parent:"incheon-23040", parentRegion:"incheon", price:9.2, dx:10, dy:-2,
      magnets:{ invest:7, commute:4, school:4, downsize:3, returnLocal:5, default:4 } },
  ];
  var WISH_CORE_MICRO_IDS = {
    "micro-hannam": true,
    "micro-cheongdam": true,
    "micro-apgujeong": true,
    "micro-daechi": true,
    "micro-banpo": true,
    "micro-jamwon": true,
    "micro-banpojamwon": true,
    "micro-seongsu": true,
    "micro-hapjeong": true,
    "micro-yeouido": true,
  };

  var WISH_PROFILES = {
    school: {
      target: 1.62, min: 1.12, max: 2.35, aspMin: 1.22, closeBias: 2.4, sameGroup: 0.8, seoulBias: 0.8,
      magnets: [
        { re: /songpa|잠실|오륜|방이/, w: 9 },
        { re: /gyeonggi-31023|분당|정자|수내|서현|판교/, w: 8 },
        { re: /gangnam|대치|도곡/, w: 8 },
        { re: /seocho|반포|잠원|서초/, w: 8 },
        { re: /yangcheon|목동/, w: 6 },
        { re: /gyeonggi-31014|광교|영통/, w: 5 },
        { re: /nowon|중계|상계/, w: 2 },
        { re: /incheon-23040|송도/, w: 3 },
      ],
    },
    commute: {
      target: 1.52, min: 1.12, max: 2.15, aspMin: 1.18, closeBias: 2.8, sameGroup: 0.4, seoulBias: 2.1,
      magnets: [
        { re: /gangnam|테헤란|역삼|삼성|대치/, w: 10 },
        { re: /yeongdeungpo|여의도/, w: 9 },
        { re: /seongdong|성수|왕십리/, w: 8 },
        { re: /mapo|상암|공덕|합정/, w: 8 },
        { re: /yongsan|용산/, w: 7 },
        { re: /gyeonggi-31023|판교|분당/, w: 7 },
        { re: /gangseo|마곡/, w: 6 },
        { re: /guro|구로디지털|금천/, w: 4 },
        { re: /gyeonggi-31014|광교|영통/, w: 5 },
        { re: /jongno|jung|종로|중구/, w: 4 },
      ],
    },
    invest: {
      target: 1.72, min: 1.12, max: 2.6, aspMin: 1.12, closeBias: 1.4, sameGroup: 0.3, seoulBias: 0.3,
      magnets: [
        { re: /gangdong|둔촌|고덕/, w: 9 },
        { re: /songpa|문정|잠실/, w: 7 },
        { re: /gyeonggi-31101|창릉|삼송|원흥|덕양/, w: 8 },
        { re: /incheon-23040|송도/, w: 8 },
        { re: /gyeonggi-31014|광교|영통/, w: 6 },
        { re: /gyeonggi-31023|판교|분당/, w: 5 },
        { re: /gyeonggi-31192|용인시기흥|기흥/, w: 6 },
        { re: /gyeonggi-31193|용인시수지|수지/, w: 5 },
        { re: /gyeonggi-31111|과천/, w: 6 },
        { re: /신도시|3기/, w: 5 },
      ],
    },
    lifestyle: {
      target: 1.62, min: 1.18, max: 2.3, aspMin: 1.2, closeBias: 1.8, sameGroup: 0.3, seoulBias: 2.0,
      magnets: [
        { re: /mapo|합정|망원|연남|상암/, w: 9 },
        { re: /seongdong|성수|왕십리/, w: 9 },
        { re: /yongsan|한남|이태원|용산/, w: 8 },
        { re: /gangnam|청담|압구정/, w: 6 },
        { re: /jongno|서촌|북촌|을지로/, w: 6 },
        { re: /gangseo|마곡/, w: 5 },
        { re: /gwangjin|광진|건대/, w: 5 },
      ],
    },
    firstBuy: {
      target: 1.3, min: 1.0, max: 1.7, aspMin: 1.06, closeBias: 2.6, sameGroup: 1.1, seoulBias: 0.5,
      magnets: [
        { re: /gangseo|마곡/, w: 8 },
        { re: /guro|구로|금천/, w: 7 },
        { re: /eunpyeong|은평/, w: 7 },
        { re: /gyeonggi-31101|창릉|삼송|원흥|덕양/, w: 7 },
        { re: /incheon-23040|송도/, w: 6 },
        { re: /gyeonggi-31014|광교|영통/, w: 6 },
        { re: /gyeonggi-31023|분당|판교/, w: 4 },
        { re: /songpa|문정/, w: 4 },
      ],
    },
    downsize: {
      target: 0.98, min: 0.82, max: 1.22, aspMin: 0.88, closeBias: 2.5, sameGroup: 1.1, seoulBias: 0.1,
      magnets: [
        { re: /gyeonggi-31023|분당|정자|수내/, w: 8 },
        { re: /yangcheon|목동/, w: 7 },
        { re: /songpa|잠실|오금/, w: 7 },
        { re: /incheon-23040|송도/, w: 4 },
        { re: /gyeonggi-31014|광교|영통/, w: 6 },
        { re: /dongjak|동작/, w: 6 },
      ],
    },
    default: {
      target: 1.52, min: 1.08, max: 2.15, aspMin: 1.15, closeBias: 2.0, sameGroup: 0.4, seoulBias: 0.9,
      magnets: [
        { re: /mapo|seongdong|songpa|gangdong|gangnam|yeongdeungpo|gyeonggi-31023|gyeonggi-31014|incheon-23040/, w: 5 },
      ],
    },
    upgrade: {
      target: 1.58, min: 1.12, max: 2.25, aspMin: 1.18, closeBias: 2.1, sameGroup: 0.5, seoulBias: 1.0,
      magnets: [
        { re: /songpa|잠실|문정/, w: 8 },
        { re: /gangdong|고덕|둔촌/, w: 7 },
        { re: /seongdong|성수/, w: 7 },
        { re: /mapo|합정|상암/, w: 6 },
        { re: /yeongdeungpo|여의도/, w: 6 },
        { re: /gyeonggi-31023|분당|판교|정자|수내/, w: 7 },
        { re: /gyeonggi-31014|광교|영통/, w: 5 },
        { re: /gangnam|대치|도곡/, w: 5 },
        { re: /seocho|반포|잠원|서초/, w: 4 },
      ],
    },
    returnLocal: {
      target: 0.98, min: 0.82, max: 1.22, aspMin: 0.88, closeBias: 2.4, sameGroup: 1.4, seoulBias: 0,
      magnets: [
        { re: /incheon-23040|송도/, w: 4 },
        { re: /gyeonggi-31023|분당|정자|수내/, w: 5 },
        { re: /gyeonggi-31014|광교|영통/, w: 5 },
        { re: /yangcheon|목동/, w: 4 },
      ],
    },
  };

  function profileMagnetScore(profile, n) {
    var s = n.id + " " + n.name;
    return (profile.magnets || []).reduce(function(total, item){
      return total + (item.re.test(s) ? item.w : 0);
    }, 0);
  }

  function microMagnetScore(n) {
    var map = n.microMagnets || {};
    return map[wishTrait] || map.default || 0;
  }

  function priceFitScore(ratio, profile) {
    var width = Math.max(0.2, (profile.max - profile.min) * 0.5);
    var fit = 1 - Math.abs(ratio - profile.target) / width;
    var score = fit * 6;
    if (ratio >= profile.min && ratio <= profile.max) score += 2;
    if (ratio > profile.max) score -= (ratio - profile.max) * 9;
    if (ratio < profile.min) score -= (profile.min - ratio) * 5;
    return score;
  }

  var CORE_ASSET_PROFILES = {
    school:      { target: 2.05, min: 1.45, max: 3.4 },
    commute:     { target: 1.95, min: 1.4,  max: 3.1 },
    invest:      { target: 2.2,  min: 1.3,  max: 3.8 },
    upgrade:     { target: 2.25, min: 1.55, max: 3.6 },
    lifestyle:   { target: 2.1,  min: 1.45, max: 3.1 },
    firstBuy:    { target: 2.0,  min: 1.35, max: 3.0 },
    downsize:    { target: 1.75, min: 1.2,  max: 2.8 },
    returnLocal: { target: 1.7,  min: 1.2,  max: 2.7 },
    default:     { target: 2.05, min: 1.4,  max: 3.1 },
  };

  var CORE_ABSOLUTE_PRICE_BASE = {
    school: 24.5,
    commute: 22.5,
    invest: 25.5,
    upgrade: 25,
    lifestyle: 22.5,
    firstBuy: 23,
    downsize: 23,
    returnLocal: 22.5,
    default: 22.5,
  };

  function coreAssetScore(ratio) {
    var profile = CORE_ASSET_PROFILES[wishTrait] || CORE_ASSET_PROFILES.default;
    var width = Math.max(0.25, (profile.max - profile.min) * 0.42);
    var fit = 1 - Math.abs(ratio - profile.target) / width;
    var score = fit * 10;
    if (ratio >= profile.min && ratio <= profile.max) score += 3;
    if (ratio < profile.min) score -= (profile.min - ratio) * 14;
    if (ratio > profile.max) score -= (ratio - profile.max) * 8;
    return score;
  }

  function coreAbsoluteAssetScore(price) {
    var base = CORE_ABSOLUTE_PRICE_BASE[wishTrait] || CORE_ABSOLUTE_PRICE_BASE.default;
    var score = (price - base) * 1.15;
    if (price >= 30) score += 4;
    else if (price >= 26) score += 2.2;
    else if (price >= 22) score += 1.2;
    if (price < 20) score -= 4.5;
    if (price < 18) score -= 2.5;
    return Math.max(-14, Math.min(14, score));
  }

  function coreLandmarkBonus(n, ratio) {
    var s = n.id + " " + n.name;
    var bonus = 0;
    if (/압구정|청담|반포|잠원|한남|대치/.test(s)) bonus += 8;
    else if (/성수|여의도/.test(s)) bonus += 4;
    else if (/합정/.test(s)) bonus += 1.5;
    if (ratio >= 1.7) bonus += 2.5;
    if (ratio >= 2.1) bonus += 2;
    return bonus;
  }

  function coreTraitPlaceBonus(n) {
    var s = n.id + " " + n.name;
    if (wishTrait === "school") {
      if (/대치|반포|잠원|압구정/.test(s)) return 8;
      if (/한남|청담/.test(s)) return 3;
      if (/합정/.test(s)) return -8;
      if (/성수|여의도/.test(s)) return -2;
      return 0;
    }
    if (wishTrait === "commute") {
      if (/여의도|성수|한남/.test(s)) return 7;
      if (/대치|청담|압구정|반포|잠원/.test(s)) return 4.5;
      if (/합정/.test(s)) return -6;
      return 0;
    }
    if (wishTrait === "invest") {
      if (/압구정|청담|반포|잠원|여의도/.test(s)) return 7;
      if (/한남|성수|대치/.test(s)) return 4.5;
      if (/합정/.test(s)) return -7;
      return 0;
    }
    if (wishTrait === "upgrade") {
      if (/압구정|반포|잠원/.test(s)) return 10;
      if (/대치|한남/.test(s)) return 8.5;
      if (/청담/.test(s)) return 6;
      if (/여의도/.test(s)) return 3.5;
      if (/성수/.test(s)) return -3.5;
      if (/합정/.test(s)) return -6;
      return 0;
    }
    if (wishTrait === "lifestyle") {
      if (/한남|성수|청담/.test(s)) return 7;
      if (/압구정|반포|잠원/.test(s)) return 4;
      if (/합정/.test(s)) return 1.5;
      return 0;
    }
    if (wishTrait === "firstBuy" || wishTrait === "downsize" || wishTrait === "returnLocal" || wishTrait === "default") {
      if (/반포|잠원|압구정/.test(s)) return 9;
      if (/한남|대치/.test(s)) return 7.5;
      if (/청담/.test(s)) return 4.5;
      if (/여의도/.test(s)) return 2.5;
      if (/성수/.test(s)) return -4;
      if (/합정/.test(s)) return -15;
      return 0;
    }
    return 0;
  }

  function aspirationFloorPenalty(ratio, profile) {
    if (!profile.aspMin || ratio >= profile.aspMin) return 0;
    return (profile.aspMin - ratio) * 12;
  }

  function luxuryPenalty(n, ratio) {
    var s = n.id + " " + n.name;
    var ultra = /gangnam|seocho|yongsan|반포|압구정|청담|잠원|대치/.test(s);
    if (!ultra) return 0;
    if (wishTrait === "invest" || wishTrait === "lifestyle") return 0;
    if (wishTrait === "school" && ratio <= 2.05) return 0;
    if (wishTrait === "commute" && ratio <= 1.95) return 0;
    if (wishTrait === "default" && ratio <= 1.8) return 0;
    if (wishTrait === "firstBuy" && ratio <= 1.45) return 0;
    return ratio > 1.8 ? (ratio - 1.8) * 6 + 1.5 : 0;
  }

  function remotePenalty(n) {
    var name = n.name || "";
    if (wishTrait === "returnLocal") return 0;
    if (wishTrait === "downsize") {
      if (/옹진|강화/.test(name)) return 6;
      if (/군$/.test(name)) return 3;
      if (/[읍면]$/.test(name)) return 2;
      return 0;
    }
    if (/옹진|강화/.test(name)) return 15;
    if (/군$/.test(name)) return 9;
    if (/[읍면]$/.test(name)) return 7;
    return 0;
  }

  function corridorBonus(n) {
    if (!originShape) return 0;
    var s = n.id + " " + n.name;
    var westOrigin = region.group === "incheon" || originShape.cx < 560;
    var eastOrigin = originShape.cx > 620;
    var bonus = 0;

    if (wishTrait === "school") {
      if (westOrigin && /yangcheon|목동|gangseo|마곡|incheon-23040|송도|gyeonggi-31101|덕양/.test(s)) bonus += 2.8;
      if (eastOrigin && /songpa|잠실|gangnam|seocho|gyeonggi-31023|분당|gyeonggi-31014|광교/.test(s)) bonus += 2.8;
    }

    if (wishTrait === "commute") {
      if (westOrigin && /yeongdeungpo|여의도|mapo|공덕|gangseo|마곡|guro|구로/.test(s)) bonus += 2.2;
      if (eastOrigin && /gangnam|seongdong|성수|songpa|잠실|gyeonggi-31023|판교|gyeonggi-31014|광교/.test(s)) bonus += 2.2;
    }

    if (wishTrait === "lifestyle") {
      if (westOrigin && /mapo|합정|망원|연남|yongsan|한남/.test(s)) bonus += 1.8;
      if (eastOrigin && /seongdong|성수|gangnam|청담|압구정/.test(s)) bonus += 1.8;
    }

    return bonus;
  }

  function centralSeoulBusinessBonus(n) {
    if (!originShape || region.group !== "seoul") return 0;
    var s = n.id + " " + n.name;
    var centralOrigin = /jongno|jung|yongsan|seodaemun|dongdaemun|seongbuk|eunpyeong|mapo/.test(region.id);
    if (!centralOrigin) return 0;

    var bonus = 0;
    if (wishTrait === "commute" || wishTrait === "upgrade") {
      if (/gangnam|대치|도곡|yeongdeungpo|여의도|seongdong|성수|yongsan|한남/.test(s)) bonus += 4.5;
      if (/seocho|반포|잠원/.test(s)) bonus += 2.5;
      if (/yangcheon|목동|gyeonggi-31023|정자|서현|판교|gyeonggi-31014|광교/.test(s)) bonus -= 3.5;
    }
    if (wishTrait === "upgrade") {
      if (/gangnam|대치|도곡|seocho|반포|잠원|seongdong|성수|yeongdeungpo|여의도/.test(s)) bonus += 3.5;
      if (/gyeonggi-31023|정자|서현|판교|gyeonggi-31014|광교/.test(s)) bonus -= 5.5;
    }
    return bonus;
  }

  // ── 3. Affinity scorer ──
  var originShape = ALL_PATHS.find(function(s){ return s.id === region.id; });
  var originPrice = syntheticPriceForId(region.id, region.group);
  var wishProfile = WISH_PROFILES[wishTrait] || WISH_PROFILES.default;

  function affinity(n) {
    var s = n.id + " " + n.name;
    var b = 0;
    if (is_.school   && SCHOOL.test(s))    b += 4;
    if (is_.commute  && COMMUTE.test(s))   b += 4;
    if (is_.lifestyle && LIFESTYLE.test(s)) b += 4;
    if (is_.invest   && INVEST.test(s))    b += 4;
    if (is_.commute  && n.region === "seoul") b += 1;
    if (is_.downsize) {
      if (n.price < originPrice) b += 3;
      if (n.price < originPrice * 0.7) b += 2;
    }
    if (is_.firstBuy) {
      if (n.price < 8) b += 2;
      if (n.price <= originPrice * 1.1) b += 1;
    }
    return b;
  }

  // ── 4. Build scored neighbors ──
  var neighbors = ALL_PATHS
    .filter(function(s){ return s.id !== region.id && s.cx != null && originShape && originShape.cx != null; })
    .map(function(s){
      var dx = s.cx - originShape.cx, dy = s.cy - originShape.cy;
      var dist = Math.sqrt(dx*dx + dy*dy);
      var price = syntheticPriceForId(s.id, s.region);
      return { id: s.id, name: s.fullName || s.name, shortName: s.name, dist: dist, price: price, region: s.region };
    });
  // Add affinity & combined score (higher is better)
  neighbors.forEach(function(n){
    n.aff = affinity(n);
    // Normalize distance: 0..1 where 0 = farthest. Invert so closer = higher score.
    n.distScore = 1 / (1 + n.dist / 50);
  });

  var sameGroup = neighbors.filter(function(n){ return n.region === region.group; });

  // ── 5. PEER — same group, similar price, persona-affinity weighted ──
  var peerPool = sameGroup.filter(function(n){ return Math.abs(n.price - originPrice) / originPrice < 0.35; });
  if (is_.downsize) {
    // Downsizer peer: cheaper same-group areas
    peerPool = sameGroup.filter(function(n){ return n.price < originPrice && n.price > originPrice * 0.5; });
    if (!peerPool.length) peerPool = sameGroup.filter(function(n){ return n.price <= originPrice; });
  }
  peerPool.sort(function(a,b){ return (b.aff - a.aff) || (a.dist - b.dist); });
  var peerCand = peerPool[0] || sameGroup[0] || neighbors.sort(function(a,b){return a.dist-b.dist;})[0];

  // ── 6. UP — higher price, persona-affinity weighted, not same as peer ──
  var upMin = is_.downsize ? 0.9 : 1.15;
  var upMax = is_.downsize ? 1.3 : 2.0;
  var upPool = neighbors
    .filter(function(n){ return n.id !== peerCand.id && n.price > originPrice * upMin && n.price < originPrice * upMax; })
    .slice(0, 30); // limit to 30 nearest
  upPool.sort(function(a,b){ return (b.aff + b.distScore * 2) - (a.aff + a.distScore * 2); });
  var upCand = upPool[0]
    || neighbors.filter(function(n){ return n.id !== peerCand.id; }).sort(function(a,b){return a.dist-b.dist;})[0]
    || neighbors[1];

  // ── 7. WISH — aspirational, persona weighted, wider radius ──
  var microCandidates = MICRO_WISH_CANDIDATES.map(function(item){
    var parent = ALL_PATHS.find(function(s){ return s.id === item.parent; });
    if (!parent || parent.cx == null || parent.cy == null) return null;
    var cx = parent.cx + (item.dx || 0);
    var cy = parent.cy + (item.dy || 0);
    var dx = cx - originShape.cx, dy = cy - originShape.cy;
    return {
      id: item.id,
      name: item.name,
      shortName: item.shortName || item.name,
      dist: Math.sqrt(dx*dx + dy*dy),
      price: item.price,
      region: item.parentRegion || parent.region,
      parentId: item.parent,
      mapRef: item.parent,
      mapCoord: [cx, cy],
      microMagnets: item.magnets || {},
      wishTier: WISH_CORE_MICRO_IDS[item.id] ? "core" : "reachable",
      aff: 0,
    };
  }).filter(Boolean);

  var wishPool = neighbors.slice(0, 60)
    .filter(function(n){
      return n.id !== peerCand.id &&
        n.id !== upCand.id &&
        n.id !== "incheon-23320" &&
        n.id !== "incheon-23310" &&
        n.id !== "incheon-23020";
    })
    .concat(microCandidates);

  wishPool.forEach(function(n){
    var ratio = n.price / originPrice;
    var magnet = profileMagnetScore(wishProfile, n) + microMagnetScore(n);
    var distanceScore = Math.max(-4, wishProfile.closeBias - (n.dist / 45));
    var sameGroupBonus = n.region === region.group ? wishProfile.sameGroup : 0;
    var seoulBonus = n.region === "seoul" ? wishProfile.seoulBias : 0;
    var outerPenalty = ratio > wishProfile.max ? (ratio - wishProfile.max) * 2.2 : 0;
    var weakMagnetPenalty = magnet > 0 ? 0 : (wishTrait === "default" ? 2.5 : 6.5);
    n.wishScore =
      (magnet * 2.15) +
      (n.aff * 2.2) +
      priceFitScore(ratio, wishProfile) +
      distanceScore +
      sameGroupBonus +
      corridorBonus(n) +
      centralSeoulBusinessBonus(n) +
      seoulBonus -
      aspirationFloorPenalty(ratio, wishProfile) -
      outerPenalty -
      weakMagnetPenalty -
      remotePenalty(n) -
      luxuryPenalty(n, ratio);
  });

  var wishReachablePool = wishPool.filter(function(n){
    return n.wishTier !== "core";
  });
  if (!wishReachablePool.length) wishReachablePool = wishPool.slice();

  wishReachablePool.sort(function(a,b){
    return (b.wishScore - a.wishScore) || (b.aff - a.aff) || (a.dist - b.dist);
  });

  var wishCand = wishReachablePool[0]
    || { name: "강남 라인", shortName: "강남", price: 15.7, dist: 100 };

  var wishCorePool = wishPool.filter(function(n){
    return n.wishTier === "core";
  });
  wishCorePool.forEach(function(n){
    var ratio = n.price / originPrice;
    n.wishCoreScore =
      (microMagnetScore(n) * 1.0) +
      (profileMagnetScore(wishProfile, n) * 0.95) +
      (n.aff * 0.45) +
      coreAssetScore(ratio) +
      coreAbsoluteAssetScore(n.price) +
      coreLandmarkBonus(n, ratio) +
      coreTraitPlaceBonus(n) +
      (n.region === "seoul" ? 2 : 0) +
      corridorBonus(n) -
      remotePenalty(n) * 0.3;
  });
  wishCorePool.sort(function(a,b){
    return (b.wishCoreScore - a.wishCoreScore) || (b.wishScore - a.wishScore) || (a.dist - b.dist);
  });
  var wishCoreCand = wishCorePool[0] || wishCand;

  // ── 8. Trait-aware narrative fragments ──
  var traitAxis = is_.school ? "학군과 교육 동선"
    : is_.commute ? "출퇴근 동선과 직주근접"
    : is_.invest ? "투자 수익률과 자산 사이클"
    : is_.downsize ? "생활 인프라와 의료 접근성"
    : is_.lifestyle ? "생활 밀도와 도시 감도"
    : is_.firstBuy ? "진입 가능한 가격대와 성장 잠재력"
    : "생활권과 자산 위계";

  var peerNarr = is_.school
      ? "학원가 반경과 통학 동선을 유지하면서 단지의 격만 한 단계 올리는 선택."
    : is_.commute
      ? "출퇴근 동선을 거의 그대로 유지하면서 주거 환경만 한 단계 올리는 후보."
    : is_.invest
      ? "비슷한 투자 프로파일과 사이클 위치를 가진 인접 권역."
    : is_.downsize
      ? "평수를 줄이고 현금 여유를 만들면서도 생활 인프라를 유지하는 자리."
    : is_.lifestyle
      ? "동네의 감도와 생활 밀도를 유지하면서 단지만 바꾸는 선택."
    : is_.firstBuy
      ? "예산 안에서 가장 현실적인 첫 매수 후보."
    : "생활권을 거의 유지하는 가장 보수적인 시나리오.";

  var upNarr = is_.school
      ? "학군과 생활권을 함께 끌어올리는 한 단계 위의 상향지."
    : is_.commute
      ? "직장과 더 가까워지면서 도시의 격이 한 단계 달라지는 자리."
    : is_.invest
      ? "다음 사이클에서 더 큰 수익을 기대할 수 있는 후보."
    : is_.downsize
      ? "비슷한 가격이지만 의료와 생활 인프라가 더 나은 자리."
    : is_.invest
      ? "자산 재편 시 가장 자연스러운 동선."
    : "같은 생활권이지만 도시의 격이 한 단계 달라지는 자리.";

  // ── 9. Metrics ──
  var peerPrice = peerCand.price, upPrice = upCand.price, wishPrice = wishCand.price;
  var gapStr = function(p) { var g = p - originPrice; return (g >= 0 ? "+" : "") + g.toFixed(1) + "억"; };
  var gapPct = function(p) { return Math.round(((p - originPrice) / originPrice) * 100); };
  var ltvLabel = function(pct) { return pct <= 15 ? "안정" : pct <= 60 ? "압박" : "불가"; };
  var timeline = function(pct) { return pct <= 15 ? "6-12개월" : pct <= 60 ? "24-36개월" : "60개월+"; };
  var commute = function(d) { return d ? "+" + Math.round(d / 3) + "분" : "~0분"; };
  var pGap = gapPct(peerPrice), uGap = gapPct(upPrice), wGap = gapPct(wishPrice);
  var peerMobility = Math.max(50, Math.min(90, 85 - Math.abs(pGap)));
  var upMobility = Math.max(25, Math.min(60, 65 - Math.abs(uGap) / 2));
  var wishMobility = Math.max(10, Math.min(30, 35 - Math.abs(wGap) / 5));
  var corePrice = wishCoreCand.price;
  var cGap = gapPct(corePrice);
  var wishCoreMobility = Math.max(6, Math.min(22, 26 - Math.abs(cGap) / 7));

  var rn = region.name;
  var pn = peerCand.shortName || peerCand.name;
  var un = upCand.shortName || upCand.name;
  var wn = wishCand.shortName || wishCand.name;
  var wcn = wishCoreCand.shortName || wishCoreCand.name;

  // ── 10. Category labels ──
  var peerCat = is_.downsize ? "평수를 줄이는 현실 후보"
    : is_.invest ? "비슷한 투자 프로파일"
    : "현재 권역 안의 다음 자리";
  var upCat = is_.school ? "한 단계 위의 상향지"
    : is_.commute ? "직장에 더 가까운 다음 자리"
    : is_.invest ? "다음 사이클의 후보"
    : is_.downsize ? "같은 가격, 더 나은 인프라"
    : "동시대 자산의 다음 챕터";

  return {
    destinations: [
      dest("peer", peerCand.name, peerCat, 68 + (seed & 15),
        persona.name + "의 " + traitAxis + "을 유지하면서 옮기는 가장 보수적인 후보.",
        { desire: 55 + (seed & 15), mobility: peerMobility, policy: 65 + (seed % 20),
          priceGap:{ value: gapStr(peerPrice), pct: Math.abs(pGap), dir: pGap >= 0 ? "up" : "down" },
          pricePsm:"—", ltv: ltvLabel(Math.abs(pGap)), timeline: timeline(Math.abs(pGap)),
          commute: commute(peerCand.dist) },
        { title:"<em>" + pn + "</em>으로의 한 칸",
          paragraphs:[
            "<span class='hl'>" + rn + "에서 " + pn + "으로의 이동</span>은 " + peerNarr,
            "인접 권역이기에 " + traitAxis + "이 깨지지 않는다. 실거주 리스크가 가장 낮은 후보." ],
          pull:{ q: pick(peerPulls, 0), cite:"에디터 노트 · " + pn } }),
      dest("up", upCand.name, upCat, 78 + (seed & 11),
        persona.name + "이 자주 호명하는 한 단계 위의 자리. " + upNarr,
        { desire: 78 + (seed & 15), mobility: upMobility, policy: 50 + (seed % 18),
          priceGap:{ value: gapStr(upPrice), pct: Math.abs(uGap), dir: "up" },
          pricePsm:"—", ltv: ltvLabel(Math.abs(uGap)), timeline: timeline(Math.abs(uGap)),
          commute: commute(upCand.dist) },
        { title:"<em>한 단계 위</em>, " + un,
          paragraphs:[
            "이 페르소나가 가장 자주 그리는 다음 좌표. <span class='hl'>" + un + "은 " + rn + "과 " + traitAxis + "은 공유하지만 위계가 다르다</span>.",
            "자기자본 격차(" + gapStr(upPrice) + ")는 분명하지만 24-36개월 내 자산 재편이 가능한 페르소나에게 실행 확률이 가장 높다." ],
          pull:{ q: pick(upPulls, 1), cite:"에디터 노트 · " + un } }),
      dest("wish", wishCand.name, "끝까지 가보고 싶은 동네", 90 + (seed & 7),
        "이 페르소나가 마음속 가장 멀리 두는 동네. " + wn + "은 가격이 아니라 정체성이 기준이 되는 자리.",
        { desire: 90 + (seed & 7), mobility: wishMobility, policy: 30 + (seed % 16),
          priceGap:{ value: gapStr(wishPrice), pct: Math.abs(wGap), dir: "up" },
          pricePsm:"—", ltv: ltvLabel(Math.abs(wGap)), timeline: timeline(Math.abs(wGap)),
          commute: commute(wishCand.dist) },
        { title:"<em>도달하고 싶은</em> " + wn,
          paragraphs:[
            wn + "은 자산 데이터에서 가장 멀리 있지만, 욕망 점수에서는 가장 가깝다. <span class='hl'>가격이 아니라 정체성이 기준</span>.",
            "이동가능성 " + wishMobility + "점. 단순 매수로는 어렵지만, 다른 두 후보를 평가하는 보이지 않는 기준점으로 작동한다." ],
          pull:{ q: pick(wishPulls, 2), cite:"에디터 노트 · " + wn } },
        { mapRef: wishCand.mapRef, mapCoord: wishCand.mapCoord }),
    ],
    graphOnlyDestinations: wishCoreCand && wishCoreCand.name !== wishCand.name ? [
      dest("wishcore", wishCoreCand.name, "카드 밖에 있는 핵심 욕망", 96 + (seed & 3),
        "실제로 당장 선택되진 않지만, 이 페르소나가 끝내 마음속 기준으로 삼는 좌표.",
        { desire: 95 + (seed & 4), mobility: wishCoreMobility, policy: 26 + (seed % 10),
          priceGap:{ value: gapStr(corePrice), pct: Math.abs(cGap), dir: "up" },
          pricePsm:"—", ltv: ltvLabel(Math.abs(cGap)), timeline: timeline(Math.abs(cGap)),
          commute: commute(wishCoreCand.dist) },
        { title:"<em>그래프 밖으로 밀리지 않는</em> 핵심 욕망",
          paragraphs:[
            wcn + "은 카드 세 장 안에는 못 들어오지만, 이 페르소나가 계속 비교 기준으로 삼는 상징 좌표다.",
            "즉시 이동 후보라기보다, 다른 후보 둘의 높낮이를 재는 보이지 않는 자다." ],
          pull:{ q: pick(wishPulls, 3), cite:"에디터 노트 · " + wcn } },
        { mapRef: wishCoreCand.mapRef, mapCoord: wishCoreCand.mapCoord, graphOnly: true }),
    ] : [],
  };
}

function reverseTargetKey(name) {
  return String(name || "")
    .toLowerCase()
    .replace(/[·•ㆍ()\-\s]/g, "")
    .replace(/[,"'`]/g, "")
    .replace(/(시|군|구)$/g, "");
}

let REVERSE_AUDIENCE_CACHE = null;

function buildReverseAudienceIndex() {
  if (REVERSE_AUDIENCE_CACHE) return REVERSE_AUDIENCE_CACHE;

  var roleMeta = {
    peer: { label: "PEER VIEW", labelKr: "현실 후보", copy: "이 동네를 지금의 대체재처럼 보는 사람들" },
    up: { label: "UPWARD STEP", labelKr: "갈 법한 상향지", copy: "다음 한 칸 위의 자리로 여기는 사람들" },
    wish: { label: "WISH REACHABLE", labelKr: "가보고 싶은 곳", copy: "마음속 가장 멀리 두는 사람들" },
  };

  var index = {};

  function personasForRegion(region) {
    var list = PERSONAS[region.id];
    if (list && list.length) return list;
    return buildGenericPersonas(region);
  }

  function reportFor(region, persona) {
    var custom = REPORTS[region.id] && REPORTS[region.id][persona.id];
    return custom || buildGenericReport(region, persona);
  }

  function touchTarget(dest) {
    var key = reverseTargetKey(dest.name);
    if (!index[key]) {
      index[key] = {
        key: key,
        targetName: dest.name,
        roles: {
          peer: { ...roleMeta.peer, total: 0, personas: {}, origins: {} },
          up: { ...roleMeta.up, total: 0, personas: {}, origins: {} },
          wish: { ...roleMeta.wish, total: 0, personas: {}, origins: {} },
        },
      };
    }
    return index[key];
  }

  REGIONS.forEach(function(region) {
    personasForRegion(region).forEach(function(persona) {
      var report = reportFor(region, persona);
      (report.destinations || []).forEach(function(dest) {
        if (!dest || !dest.role || !roleMeta[dest.role]) return;
        var target = touchTarget(dest);
        var roleBucket = target.roles[dest.role];
        var personaKey = persona.id;
        var originKey = region.id;

        roleBucket.total += 1;

        if (!roleBucket.personas[personaKey]) {
          roleBucket.personas[personaKey] = {
            id: persona.id,
            name: persona.name,
            emoji: persona.emoji,
            tagline: persona.tagline,
            count: 0,
            origins: {},
          };
        }
        roleBucket.personas[personaKey].count += 1;
        roleBucket.personas[personaKey].origins[originKey] = (roleBucket.personas[personaKey].origins[originKey] || 0) + 1;

        roleBucket.origins[originKey] = roleBucket.origins[originKey] || { id: region.id, name: region.name, count: 0 };
        roleBucket.origins[originKey].count += 1;
      });
    });
  });

  Object.keys(index).forEach(function(key) {
    var target = index[key];
    Object.keys(target.roles).forEach(function(role) {
      var bucket = target.roles[role];
      bucket.personaList = Object.values(bucket.personas)
        .map(function(persona) {
          var topOrigins = Object.entries(persona.origins)
            .map(function(entry) {
              var originId = entry[0];
              var count = entry[1];
              var region = REGIONS.find(function(r) { return r.id === originId; });
              return region ? { id: originId, name: region.name, count: count } : null;
            })
            .filter(Boolean)
            .sort(function(a, b) { return b.count - a.count; })
            .slice(0, 3);
          return {
            id: persona.id,
            name: persona.name,
            emoji: persona.emoji,
            tagline: persona.tagline,
            count: persona.count,
            share: bucket.total ? Math.round((persona.count / bucket.total) * 100) : 0,
            topOrigins: topOrigins,
          };
        })
        .sort(function(a, b) {
          return (b.count - a.count) || a.name.localeCompare(b.name, "ko");
        });
      bucket.originList = Object.values(bucket.origins)
        .sort(function(a, b) { return b.count - a.count; });
      delete bucket.personas;
      delete bucket.origins;
    });
  });

  REVERSE_AUDIENCE_CACHE = index;
  return REVERSE_AUDIENCE_CACHE;
}

function getReverseAudienceForTarget(name) {
  var index = buildReverseAudienceIndex();
  return index[reverseTargetKey(name)] || null;
}

window.__APP_DATA__ = {
  REGIONS,
  SEOUL_SHAPES,
  PERSONAS,
  REPORTS,
  buildGenericReport,
  buildGenericPersonas,
  buildReverseAudienceIndex,
  getReverseAudienceForTarget,
};
