Header image for test project

test project

Prompt

You are the lead software architect, principal full stack engineer, quantitative researcher, GIS specialist, data engineer, machine learning engineer, property analyst, urban planning researcher, UX designer and systems architect responsible for building the world's most advanced Australian residential property investment intelligence platform. Continuously improve it. Maintain the visual identity while dramatically improving architecture, usability, scalability, responsiveness and performance. The HTML should become the presentation layer only. Business logic should progressively move into modular backend services. Your objective is NOT to build another property search website. Your objective is to build the Bloomberg Terminal for Australian residential property investing. Every design decision should maximise analytical accuracy, investment intelligence, maintainability and future extensibility. Development Philosophy Think like an institutional investment fund. Never choose the simplest solution merely because it is faster. Whenever multiple approaches exist: Research each approach. Compare advantages and disadvantages. Select the strongest methodology. Implement it. Refactor existing code whenever a better architecture becomes apparent. Never accumulate technical debt unnecessarily. Continuously improve every component you touch. Whenever a better algorithm, UI pattern, valuation methodology or engineering practice exists, adopt it. Always favour correctness over convenience. Research Requirements Before implementing any significant feature: Research current industry best practice. Research academic literature where appropriate. Research quantitative finance methodologies. Research professional property valuation techniques. Research GIS analysis methods. Research urban planning methodologies. Research machine learning techniques. Research Australian property market practices. Research institutional investment processes. Implement the strongest solution rather than the easiest solution. Live Data Philosophy Nothing should depend on static datasets. Whenever the application loads, refreshes or performs a property search, automatically retrieve fresh data wherever possible. Every dataset should include: Last updated timestamp Data source Confidence level Quality score Fallback behaviour Graceful handling of unavailable data Every model should automatically recalculate after new information arrives. System Architecture Separate the platform into modular services. Frontend Backend API Research Engine Data Collection Engine Property Valuation Engine Market Intelligence Engine GIS Engine Infrastructure Engine Prediction Engine Portfolio Engine Opportunity Detection Engine Database Caching Layer Background Workers Scheduler Every module should operate independently with well-defined interfaces. Research Engine Continuously gather data from every legally available source including but not limited to: CoreLogic Domain RealEstate.com.au ABS RBA OpenStreetMap Sydney Metro Transport for NSW Infrastructure Australia Council Development Applications Council LEPs Council DCPs School rankings NAPLAN Hospital developments Infrastructure announcements Government budgets Population growth Migration Employment growth Rental vacancies Auction clearance rates Sales velocity Inventory Construction pipelines Housing supply Building approvals Commercial developments Industrial developments Rezoning proposals Environmental overlays Flood mapping Bushfire mapping Noise mapping Flight paths Internet infrastructure Medical facilities Universities Shopping centres Employment hubs Owner occupier ratios Investor ownership Short stay accommodation density Crime statistics Mortgage stress Interest rates Construction costs Insurance costs Store every research result with timestamps and provenance. Property Valuation Engine When a property address is entered calculate: Current fair value Likely market value Conservative valuation Aggressive valuation Comparable sales valuation Land value Building replacement cost Rental valuation Rental yield Future rental yield Capital growth expectation Risk adjusted return Negotiation range Confidence interval Probability the property is overpriced Probability the property is underpriced Future infrastructure premium School premium Street premium View premium Solar orientation Noise penalty Flood penalty Slope Elevation Subdivision potential Dual occupancy potential Granny flat potential Development potential Highest and best use Future zoning uplift Holding costs Transaction costs Expected cash flow Sensitivity to interest rates Explain every calculation. Nothing should be a black box. Market Intelligence Engine Analyse every suburb continuously. Generate: Investment Score Growth Score Yield Score Risk Score Infrastructure Score Demand Score Supply Score Liquidity Score Lifestyle Score School Score Transport Score Government Investment Score Development Score Climate Resilience Score Rental Demand Score Mortgage Stress Score Institutional Interest Score Opportunity Score Confidence Score Every score must explain precisely why it exists. Opportunity Detection Engine The system should not simply rank suburbs. It should actively discover investment opportunities. Examples include: Undervalued suburbs Undervalued streets Undervalued individual properties Infrastructure not yet reflected in prices Rezoning opportunities School catchment improvements Future transport improvements Future employment hubs Emerging lifestyle precincts Rental shortages Developer accumulation Government spending concentration Supply shortages Rapid inventory declines Strong rental demand Commercial expansion Gentrification Neighbourhood momentum Each opportunity should include evidence, expected impact and confidence. GIS Platform Turn the existing map into a professional GIS platform. Support interactive layers including: Metro Future Metro Heavy Rail Future Rail Light Rail Motorways Future Roads Schools School Catchments Hospitals Medical Facilities Universities Shopping Centres Employment Hubs Flood Zones Bushfire Zones Noise Maps Flight Paths Crime Heatmaps Development Applications Rezoning Infrastructure Projects Rental Demand Heatmaps Growth Heatmaps Yield Heatmaps Supply Heatmaps Opportunity Heatmaps Institutional Interest Heatmaps Every layer should be independently toggleable. Predictive Analytics Do not use simple weighted averages. Create an ensemble prediction engine. Independent models should analyse: Growth Yield Risk Infrastructure Supply Demand Liquidity Government Investment School Performance Transport Accessibility Economic Growth Population Growth Climate Risk Development Activity Combine all models into a final prediction. Produce confidence intervals. Support forecasting for: 1 year 3 years 5 years 10 years 15 years Generate Base, Bull and Bear scenarios. Portfolio Analytics Allow management of owned properties. Track: Current value Equity Debt Rental income Cash flow Yield Capital growth Diversification Risk Net worth Loan to Value Ratio Projected wealth Portfolio allocation Geographic concentration Sector concentration Future performance Market Intelligence Dashboard The homepage should become an investment dashboard rather than a search page. Display: Top investment opportunities Highest conviction opportunities Biggest market changes Infrastructure announcements Rezoning announcements New development approvals Market regime indicator Supply and demand indicators Capital flow indicators Interest rate outlook Construction activity Market momentum Portfolio summary Watchlist Recent changes since last refresh Daily automatically generated investment summary Explainability Every score should be transparent. Every recommendation should explain: Why it exists. What data supports it. What assumptions were made. What risks exist. How confident the model is. What could invalidate the prediction. Performance Optimise aggressively. Use caching. Background workers. Parallel processing. Incremental updates. Database indexing. Lazy loading. Asynchronous processing. Streaming updates where appropriate. The interface should remain responsive while research continues. UI and UX Maintain the existing premium aesthetic. Improve rather than redesign. Follow modern UX principles. Support desktop first while remaining fully responsive. Use subtle animations. Micro interactions. Loading skeletons. Progress indicators. Accessible colour palettes. Keyboard shortcuts. Professional data visualisation. Interactive charts. Responsive GIS interactions. Institutional quality presentation. Continuous Improvement Do not stop after completing one feature. Continuously: Research. Improve. Refactor. Optimise. Expand. Replace inferior implementations with stronger ones. Whenever you identify a significantly better architecture or methodology, adopt it. The final objective is to create the definitive Australian residential property intelligence platform that surpasses every commercially available alternative in analytical depth, investment intelligence and usability.

A system prompt was added to support web rendering

Answer guidance

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <title>Meridian v3 · Sydney Property Alpha</title> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@500;600;700&family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet"> <style> :root{ --ink:#0b1017; --ink-2:#111a24; --ink-3:#17222f; --ink-4:#1e2c3b; --line:#26384a; --line-2:#31465b; --fog:#7d92a8; --fog-2:#5f7387; --paper:#e9eff5; --paper-dim:#c3d0dd; --amber:#eab34c; --amber-2:#f0c877; --amber-deep:#c9902f; --teal:#37b6a4; --future:#6ea6ff; --road:#d98a5b; --coral:#e5695c; --green:#5fbf8a; --violet:#9b8cf0; --water:#0c1a26; --land:#0e1620; --shadow: 0 10px 40px -12px rgba(0,0,0,.6); --fs-display:'Space Grotesk', ui-sans-serif, system-ui, sans-serif; --fs-body:'IBM Plex Sans', ui-sans-serif, system-ui, sans-serif; --fs-mono:'IBM Plex Mono', ui-monospace, 'SF Mono', Menlo, monospace; } *{box-sizing:border-box} html,body{margin:0;height:100%} body{background:var(--ink);color:var(--paper);font-family:var(--fs-body);font-size:14px;line-height:1.45; -webkit-font-smoothing:antialiased;overflow:hidden} ::selection{background:var(--amber);color:#161007} button{font-family:inherit;cursor:pointer;color:inherit} input,select{font-family:inherit;color:inherit} .mono{font-family:var(--fs-mono)} .app{display:flex;flex-direction:column;height:100vh;height:100dvh} header.top{display:flex;align-items:center;gap:16px;padding:10px 18px;border-bottom:1px solid var(--line); background:linear-gradient(180deg,var(--ink-2),var(--ink));z-index:40;flex:0 0 auto} .brand{display:flex;align-items:center;gap:11px;min-width:0} .brand .mark{width:34px;height:34px;border-radius:9px;flex:0 0 auto;background:radial-gradient(120% 120% at 30% 20%,#20303f,#0d1720); border:1px solid var(--line-2);display:grid;place-items:center;box-shadow:inset 0 0 0 1px rgba(234,179,76,.08)} .brand .mark svg{width:22px;height:22px} .brand h1{font-family:var(--fs-display);font-size:16px;letter-spacing:.14em;font-weight:600;margin:0;text-transform:uppercase} .brand .sub{font-size:10px;letter-spacing:.28em;color:var(--amber);text-transform:uppercase;margin-top:1px;font-family:var(--fs-mono)} .top .spacer{flex:1} .seg{display:flex;background:var(--ink-3);border:1px solid var(--line);border-radius:10px;padding:3px;gap:2px} .seg button{border:0;background:transparent;color:var(--fog);padding:6px 12px;border-radius:7px;font-size:12px;font-weight:500;white-space:nowrap;transition:.15s} .seg button.on{background:var(--amber);color:#191203;font-weight:600} .seg button:not(.on):hover{color:var(--paper);background:var(--ink-4)} .search{display:flex;align-items:center;gap:8px;background:var(--ink-3);border:1px solid var(--line);border-radius:10px; padding:0 12px;height:38px;min-width:210px;max-width:300px;flex:0 1 280px} .search:focus-within{border-color:var(--line-2);box-shadow:0 0 0 3px rgba(110,166,255,.10)} .search svg{width:16px;height:16px;color:var(--fog);flex:0 0 auto} .search input{border:0;background:transparent;outline:0;width:100%;font-size:13px;color:var(--paper)} .search input::placeholder{color:var(--fog-2)} /* pulse bar */ .pulse{display:flex;align-items:center;gap:14px;padding:6px 18px;border-bottom:1px solid var(--line); background:var(--ink-2);font-family:var(--fs-mono);font-size:10.5px;color:var(--fog);overflow-x:auto;white-space:nowrap;flex:0 0 auto} .pulse b{color:var(--paper);font-weight:500} .pulse .up{color:var(--green)} .pulse .dn{color:var(--coral)} .pulse .tag{padding:1px 7px;border-radius:5px;background:rgba(229,105,92,.14);color:var(--coral);letter-spacing:.06em} .pulse .sep{color:var(--line-2)} .pulse .scen{color:var(--future)} .pulse button{margin-left:6px;background:var(--ink-3);border:1px solid var(--line);border-radius:7px; padding:3px 10px;font-size:10.5px;color:var(--amber-2);font-family:var(--fs-mono);display:flex;align-items:center;gap:6px;flex:0 0 auto} .pulse button:first-of-type{margin-left:auto} .pulse button:hover{border-color:var(--amber)} .pulse button.ghost{color:var(--fog)} .pulse button svg{width:11px;height:11px} @keyframes spin{to{transform:rotate(360deg)}} .spinning svg{animation:spin .6s linear} .body{flex:1;display:grid;grid-template-columns:308px 1fr 384px;min-height:0} aside.panel{background:var(--ink-2);border-right:1px solid var(--line);overflow-y:auto;min-height:0} aside.right{border-right:0;border-left:1px solid var(--line)} .stage{position:relative;min-width:0;background:var(--ink);overflow:hidden} .panel::-webkit-scrollbar,.right::-webkit-scrollbar,.dbody::-webkit-scrollbar,.rlist::-webkit-scrollbar{width:9px} .panel::-webkit-scrollbar-thumb,.right::-webkit-scrollbar-thumb,.dbody::-webkit-scrollbar-thumb,.rlist::-webkit-scrollbar-thumb{background:var(--line);border-radius:9px;border:2px solid var(--ink-2)} .sect{border-bottom:1px solid var(--line);padding:15px 16px} .sect h2{font-family:var(--fs-mono);font-size:11px;letter-spacing:.22em;text-transform:uppercase;color:var(--fog); margin:0 0 13px;display:flex;align-items:center;gap:8px} .sect h2 .idx{color:var(--amber);font-weight:600} .sect h2 .hint{margin-left:auto;color:var(--fog-2);letter-spacing:.05em;font-size:10px;text-transform:none;font-family:var(--fs-body)} .sect h2 .caret{margin-left:auto;transition:.2s;color:var(--fog-2)} .sect.collapsed .caret{transform:rotate(-90deg)} .sect.collapsed .sbody{display:none} .sect h2.click{cursor:pointer;user-select:none} .field{margin-bottom:12px}.field:last-child{margin-bottom:0} .field label{display:block;font-size:11px;color:var(--paper-dim);margin-bottom:6px} .frow{display:grid;grid-template-columns:1fr 1fr;gap:9px} .chips{display:flex;flex-wrap:wrap;gap:6px} .chip{border:1px solid var(--line);background:var(--ink-3);color:var(--fog);border-radius:8px;padding:7px 11px; font-size:12px;transition:.14s;flex:1;text-align:center;min-width:fit-content} .chip.on{background:var(--ink-4);border-color:var(--amber);color:var(--amber-2)} .chip:not(.on):hover{color:var(--paper);border-color:var(--line-2)} .chip.sm{padding:6px 0;font-size:12px} select.inp{width:100%;height:36px;background:var(--ink-3);border:1px solid var(--line);border-radius:8px; padding:0 10px;font-size:12.5px;outline:0;appearance:none; background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%237d92a8' stroke-width='2.5'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E"); background-repeat:no-repeat;background-position:right 10px center} select.inp:focus{border-color:var(--line-2)} .money{position:relative} .money input{width:100%;height:36px;background:var(--ink-3);border:1px solid var(--line);border-radius:8px; padding:0 10px 0 24px;font-size:13px;outline:0;font-family:var(--fs-mono)} .money:before{content:"$";position:absolute;left:10px;top:9px;color:var(--fog);font-family:var(--fs-mono);font-size:13px} .money input:focus{border-color:var(--line-2)} /* buying power output */ .bp{background:linear-gradient(160deg,rgba(234,179,76,.10),rgba(234,179,76,.02)),var(--ink-3); border:1px solid rgba(234,179,76,.35);border-radius:12px;padding:13px 14px;margin-top:4px} .bp .lab{font-size:9.5px;color:var(--fog);text-transform:uppercase;letter-spacing:.16em;font-family:var(--fs-mono)} .bp .big{font-family:var(--fs-display);font-weight:700;font-size:25px;color:var(--amber-2);margin:3px 0 1px;letter-spacing:-.01em} .bp .sub2{font-size:10.5px;color:var(--fog)} .bp .rows{margin-top:10px;border-top:1px solid rgba(234,179,76,.18);padding-top:9px} .bp .r{display:flex;justify-content:space-between;font-size:11px;color:var(--paper-dim);margin-top:4px} .bp .r b{font-family:var(--fs-mono);font-weight:500;color:var(--paper)} .bp .flag{display:inline-flex;align-items:center;gap:5px;font-size:10px;margin-top:8px;padding:3px 8px;border-radius:6px; background:rgba(95,191,138,.13);color:var(--green);font-family:var(--fs-mono)} .bp .flag.warn{background:rgba(229,105,92,.13);color:var(--coral)} .bp button{width:100%;margin-top:11px;background:var(--amber);border:0;border-radius:8px;padding:8px; font-weight:600;font-size:12px;color:#191203} .bp button:hover{background:var(--amber-2)} .wrow{display:flex;align-items:center;gap:10px;margin-bottom:10px} .wrow .wl{flex:0 0 118px;font-size:11.5px;color:var(--paper-dim);display:flex;align-items:center;gap:6px} .wrow .dot{width:8px;height:8px;border-radius:50%;flex:0 0 auto} .wrow input[type=range]{flex:1;-webkit-appearance:none;height:4px;border-radius:4px;background:var(--ink-4);outline:0} .wrow input[type=range]::-webkit-slider-thumb{-webkit-appearance:none;width:14px;height:14px;border-radius:50%; background:var(--amber);border:2px solid var(--ink-2);cursor:grab;box-shadow:0 1px 4px rgba(0,0,0,.4)} .wrow input[type=range]::-moz-range-thumb{width:14px;height:14px;border-radius:50%;background:var(--amber);border:2px solid var(--ink-2);cursor:grab} .wrow .wv{flex:0 0 26px;text-align:right;font-family:var(--fs-mono);font-size:11px;color:var(--fog)} .toggle{display:flex;align-items:center;gap:10px;padding:8px 0;cursor:pointer;user-select:none} .toggle .sw{width:34px;height:19px;border-radius:20px;background:var(--ink-4);border:1px solid var(--line);position:relative;flex:0 0 auto;transition:.16s} .toggle .sw:after{content:"";position:absolute;width:13px;height:13px;border-radius:50%;background:var(--fog);top:2px;left:2px;transition:.16s} .toggle.on .sw{background:rgba(234,179,76,.22);border-color:var(--amber)} .toggle.on .sw:after{left:16px;background:var(--amber)} .toggle .tl{font-size:12.5px;flex:1} .toggle .swatch{width:22px;height:0;border-top-width:3px;border-top-style:solid;border-radius:3px;flex:0 0 auto} .toggle .swatch.dash{border-top-style:dashed} .toggle .gl{width:11px;height:11px;border-radius:3px;flex:0 0 auto} .note{font-size:11px;color:var(--fog-2);line-height:1.5} /* map */ #map{width:100%;height:100%;display:block;cursor:grab;touch-action:none} #map.drag{cursor:grabbing} .maptop{position:absolute;top:12px;left:14px;right:14px;display:flex;gap:10px;align-items:flex-start;pointer-events:none;z-index:5} .legend{background:rgba(13,20,28,.82);backdrop-filter:blur(8px);border:1px solid var(--line);border-radius:10px;padding:10px 12px;pointer-events:auto;max-width:210px} .legend h3{margin:0 0 8px;font-size:10px;letter-spacing:.18em;text-transform:uppercase;color:var(--fog);font-family:var(--fs-mono)} .lgrad{height:9px;border-radius:5px;margin-bottom:4px;background:linear-gradient(90deg,#3a4a78,#2fa39a 52%,#eab34c 82%,#f0894e)} .lgl{display:flex;justify-content:space-between;font-size:9.5px;color:var(--fog);font-family:var(--fs-mono)} .lgrow{display:flex;align-items:center;gap:7px;font-size:10.5px;color:var(--paper-dim);margin-top:6px} .lgrow .swatch{width:18px;border-top-width:3px;border-top-style:solid;border-radius:2px} .lgrow .swatch.dash{border-top-style:dashed} .mapctl{position:absolute;bottom:14px;right:14px;display:flex;flex-direction:column;gap:6px;z-index:5} .mapctl button{width:34px;height:34px;border-radius:9px;background:rgba(13,20,28,.85);border:1px solid var(--line); color:var(--paper);font-size:17px;display:grid;place-items:center;transition:.14s} .mapctl button:hover{border-color:var(--amber);color:var(--amber)} .scalebar{position:absolute;bottom:16px;left:16px;z-index:5;font-family:var(--fs-mono);font-size:9.5px;color:var(--fog);pointer-events:none} .scalebar .bar{height:4px;border:1px solid var(--fog);border-top:0;width:70px;margin-top:2px} .credit{position:absolute;bottom:6px;left:50%;transform:translateX(-50%);font-size:9px;color:var(--fog-2);z-index:4;pointer-events:none;font-family:var(--fs-mono);letter-spacing:.08em} .tip{position:absolute;pointer-events:none;z-index:30;background:rgba(9,14,20,.95);border:1px solid var(--line-2); border-radius:9px;padding:9px 11px;min-width:160px;opacity:0;transition:opacity .12s;box-shadow:var(--shadow)} .tip.show{opacity:1} .tip .tn{font-family:var(--fs-display);font-weight:600;font-size:13px;margin-bottom:1px} .tip .tr{font-size:10px;color:var(--fog);text-transform:uppercase;letter-spacing:.1em;margin-bottom:6px;font-family:var(--fs-mono)} .tip .trow{display:flex;justify-content:space-between;gap:14px;font-size:11px;margin-top:2px} .tip .trow span{color:var(--fog)} .tip .trow b{font-family:var(--fs-mono);font-weight:500} .marker{cursor:pointer} .mk-lab{font-family:var(--fs-body);font-size:9.5px;fill:var(--paper-dim);pointer-events:none;paint-order:stroke; stroke:rgba(6,10,15,.9);stroke-width:3px;font-weight:500} /* rank */ .rhead{padding:13px 16px;border-bottom:1px solid var(--line);display:flex;align-items:center;gap:10px} .rhead h2{font-family:var(--fs-mono);font-size:11px;letter-spacing:.2em;text-transform:uppercase;color:var(--fog);margin:0} .rhead .count{margin-left:auto;font-family:var(--fs-mono);font-size:11px;color:var(--amber)} .rfilt{display:flex;gap:6px;padding:8px 12px 0} .rfilt input{flex:1;min-width:0;height:28px;background:var(--ink-3);border:1px solid var(--line);border-radius:7px;padding:0 9px;font-size:11px;outline:0;color:var(--paper)} .rfilt input:focus{border-color:var(--line-2)} .rsort{display:flex;gap:4px;padding:9px 12px;border-bottom:1px solid var(--line);flex-wrap:wrap} .rsort button{font-size:10.5px;color:var(--fog);background:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:4px 8px} .rsort button.on{color:var(--amber-2);border-color:var(--amber);background:var(--ink-4)} .rlist{overflow-y:auto} .ritem{display:flex;align-items:center;gap:10px;padding:11px 14px;border-bottom:1px solid rgba(38,56,74,.5); cursor:pointer;transition:.12s;position:relative} .ritem:hover{background:var(--ink-3)} .ritem.sel{background:linear-gradient(90deg,rgba(234,179,76,.10),transparent);box-shadow:inset 3px 0 0 var(--amber)} .ritem.dim{opacity:.42} .rk{font-family:var(--fs-mono);font-size:12px;color:var(--fog);width:20px;text-align:right;flex:0 0 auto} .rmid{flex:1;min-width:0} .rnm{font-family:var(--fs-display);font-weight:600;font-size:14px;display:flex;align-items:center;gap:7px} .rnm .pin{width:14px;height:14px;opacity:.5;flex:0 0 auto} .rnm .pin.on{opacity:1} .rnm .ext{width:13px;height:13px;color:var(--fog-2);flex:0 0 auto} .rnm .ext:hover{color:var(--future)} .rmeta{font-size:10.5px;color:var(--fog);margin-top:2px;display:flex;gap:8px;flex-wrap:wrap;align-items:center} .rmeta .badge{font-family:var(--fs-mono);font-size:9px;letter-spacing:.04em;padding:1px 5px;border-radius:4px;text-transform:uppercase} .b-fut{background:rgba(110,166,255,.16);color:var(--future)} .b-open{background:rgba(55,182,164,.16);color:var(--teal)} .fcv{font-family:var(--fs-mono);font-size:10.5px} .fcv.up{color:var(--green)}.fcv.dn{color:var(--coral)} .rbar{width:48px;flex:0 0 auto} .rbar .bg{height:5px;background:var(--ink-4);border-radius:4px;overflow:hidden} .rbar .fl{height:100%;border-radius:4px} .rsc{font-family:var(--fs-mono);font-weight:600;font-size:16px;width:32px;text-align:right;flex:0 0 auto} /* detail */ .detail{display:none;flex-direction:column;height:100%} .detail.on{display:flex} .dhead{padding:14px 16px 12px;border-bottom:1px solid var(--line)} .back{display:inline-flex;align-items:center;gap:6px;font-size:11px;color:var(--fog);background:none;border:0;padding:0;margin-bottom:10px} .back:hover{color:var(--amber)} .dtitle{display:flex;align-items:flex-start;gap:12px} .dtitle .dn{font-family:var(--fs-display);font-weight:700;font-size:22px;line-height:1.05} .dtitle .dr{font-size:11px;color:var(--fog);text-transform:uppercase;letter-spacing:.1em;font-family:var(--fs-mono);margin-top:3px} .dgrade{margin-left:auto;text-align:center;flex:0 0 auto} .dgrade .g{font-family:var(--fs-display);font-weight:700;font-size:30px;line-height:1} .dgrade .gl{font-size:9px;color:var(--fog);text-transform:uppercase;letter-spacing:.12em;font-family:var(--fs-mono)} .dscorebar{display:flex;align-items:center;gap:10px;margin-top:12px} .dscorebar .num{font-family:var(--fs-mono);font-weight:600;font-size:13px;flex:0 0 auto} .dscorebar .track{flex:1;height:7px;background:var(--ink-4);border-radius:5px;overflow:hidden} .dscorebar .track .f{height:100%;border-radius:5px} .dscorebar .rank{font-family:var(--fs-mono);font-size:11px;color:var(--amber)} .dbody{overflow-y:auto;flex:1} .dsec{padding:15px 16px;border-bottom:1px solid var(--line)} .dsec h3{font-family:var(--fs-mono);font-size:10.5px;letter-spacing:.2em;text-transform:uppercase;color:var(--fog);margin:0 0 12px;display:flex;align-items:center} .dsec h3 .r{margin-left:auto;font-size:9.5px;color:var(--fog-2);text-transform:none;letter-spacing:.03em} .radar{display:block;margin:0 auto} .fact{display:flex;align-items:center;gap:9px;margin-bottom:9px} .fact .fl{flex:0 0 96px;font-size:11px;color:var(--paper-dim);display:flex;align-items:center;gap:6px} .fact .dot{width:7px;height:7px;border-radius:50%;flex:0 0 auto} .fact .tr{flex:1;height:6px;background:var(--ink-4);border-radius:4px;overflow:hidden} .fact .tr .f{height:100%;border-radius:4px} .fact .fv{flex:0 0 26px;text-align:right;font-family:var(--fs-mono);font-size:11px;color:var(--fog)} .kv{display:grid;grid-template-columns:1fr 1fr;gap:10px} .kv .cell{background:var(--ink-3);border:1px solid var(--line);border-radius:9px;padding:10px 11px} .kv .cell .k{font-size:9.5px;color:var(--fog);text-transform:uppercase;letter-spacing:.1em;font-family:var(--fs-mono);margin-bottom:4px} .kv .cell .v{font-family:var(--fs-mono);font-weight:600;font-size:16px} .kv .cell .v small{font-size:10px;color:var(--fog);font-weight:400} .narr{font-size:12.5px;color:var(--paper-dim);line-height:1.6} .hl{display:flex;flex-direction:column;gap:8px;margin-top:4px} .hl .row{display:flex;align-items:center;gap:9px;font-size:12px} .hl .ic{width:24px;height:24px;border-radius:7px;display:grid;place-items:center;flex:0 0 auto} .hl .row b{font-weight:600} .hl .row .yr{margin-left:auto;font-family:var(--fs-mono);font-size:10px;padding:2px 6px;border-radius:5px;background:rgba(110,166,255,.14);color:var(--future)} /* forecast card */ .fcard{background:var(--ink-3);border:1px solid var(--line);border-radius:11px;padding:12px 13px} .fcard .top{display:flex;align-items:baseline;gap:10px} .fcard .fnum{font-family:var(--fs-display);font-weight:700;font-size:26px} .fcard .frange{font-family:var(--fs-mono);font-size:10.5px;color:var(--fog)} .fcard .flabel{font-size:10px;color:var(--fog);text-transform:uppercase;letter-spacing:.12em;font-family:var(--fs-mono);margin-bottom:4px} .fcard .fbar{position:relative;height:26px;margin:10px 0 2px} .fcard .axis{position:absolute;top:12px;left:0;right:0;height:2px;background:var(--ink-4)} .fcard .zero{position:absolute;top:6px;width:1px;height:14px;background:var(--fog-2)} .fcard .band{position:absolute;top:9px;height:8px;border-radius:4px;opacity:.9} .fcard .pt{position:absolute;top:7px;width:12px;height:12px;border-radius:50%;border:2px solid var(--ink-2);transform:translateX(-50%)} .fcard .ticks{display:flex;justify-content:space-between;font-family:var(--fs-mono);font-size:9px;color:var(--fog-2)} .fcard .three{display:flex;justify-content:space-between;margin-top:10px;padding-top:9px;border-top:1px solid var(--line);font-size:11.5px;color:var(--paper-dim)} .fcard .three b{font-family:var(--fs-mono)} .lbtns{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-top:11px} .lbtn{display:flex;align-items:center;justify-content:center;gap:7px;background:var(--ink-4);border:1px solid var(--line-2); border-radius:9px;padding:9px 6px;font-size:12px;font-weight:600;text-decoration:none;color:var(--paper);transition:.15s} .lbtn:hover{border-color:var(--amber);color:var(--amber-2)} .lbtn svg{width:13px;height:13px} .lcap{font-size:10.5px;color:var(--fog-2);margin-top:8px;text-align:center} .estgrid{display:grid;grid-template-columns:1fr 1fr;gap:9px;margin-bottom:12px} .estgrid .field{margin:0} .estout{background:linear-gradient(180deg,var(--ink-3),var(--ink-2));border:1px solid var(--line-2);border-radius:12px;padding:14px} .estout .lab{font-size:10px;color:var(--fog);text-transform:uppercase;letter-spacing:.14em;font-family:var(--fs-mono)} .estout .big{font-family:var(--fs-display);font-weight:700;font-size:27px;margin:4px 0 2px;letter-spacing:-.01em} .estout .rng{font-family:var(--fs-mono);font-size:11px;color:var(--fog)} .verdict{margin-top:12px;border-radius:10px;padding:11px 12px;display:flex;gap:10px;align-items:center} .verdict .vic{width:30px;height:30px;border-radius:8px;flex:0 0 auto;display:grid;place-items:center;font-family:var(--fs-mono);font-weight:600} .verdict .vt{font-weight:600;font-size:13px} .verdict .vs{font-size:11px;color:var(--paper-dim);margin-top:1px} .yieldline{display:flex;justify-content:space-between;margin-top:11px;font-size:11.5px;color:var(--paper-dim);padding-top:11px;border-top:1px solid var(--line)} .yieldline b{font-family:var(--fs-mono)} .cmptray{position:absolute;bottom:14px;left:50%;transform:translateX(-50%);z-index:6;display:flex;gap:8px; background:rgba(13,20,28,.9);backdrop-filter:blur(8px);border:1px solid var(--line-2);border-radius:12px;padding:8px 9px; box-shadow:var(--shadow);max-width:calc(100% - 40px)} .cmptray.hide{display:none} .cmpcard{background:var(--ink-3);border:1px solid var(--line);border-radius:8px;padding:7px 10px;min-width:120px;position:relative} .cmpcard .x{position:absolute;top:4px;right:5px;color:var(--fog);font-size:12px;line-height:1;cursor:pointer} .cmpcard .x:hover{color:var(--coral)} .cmpcard .cn{font-family:var(--fs-display);font-weight:600;font-size:12.5px;padding-right:12px} .cmpcard .cs{font-family:var(--fs-mono);font-size:11px;color:var(--fog);margin-top:2px} .cmpcard .cs b{color:var(--amber-2)} .cmphint{align-self:center;font-size:10.5px;color:var(--fog);padding:0 6px;max-width:120px} /* sources modal */ .modal{position:fixed;inset:0;background:rgba(5,9,14,.72);backdrop-filter:blur(4px);z-index:80;display:none;align-items:center;justify-content:center;padding:20px} .modal.on{display:flex} .mbox{background:var(--ink-2);border:1px solid var(--line-2);border-radius:14px;max-width:620px;width:100%;max-height:82vh; overflow-y:auto;padding:22px 24px;box-shadow:var(--shadow)} .mbox h2{font-family:var(--fs-display);font-size:18px;margin:0 0 4px} .mbox .md{font-size:11px;color:var(--fog);font-family:var(--fs-mono);margin-bottom:16px} .src{display:flex;gap:11px;padding:10px 0;border-top:1px solid var(--line);font-size:12.5px} .src .n{font-family:var(--fs-mono);color:var(--amber);flex:0 0 24px;font-size:11px} .src .t b{display:block;font-weight:600;margin-bottom:2px} .src .t span{color:var(--fog);font-size:11.5px;line-height:1.5} .mbox .close{margin-top:16px;width:100%;background:var(--ink-3);border:1px solid var(--line);border-radius:9px;padding:9px;color:var(--paper);font-size:12.5px} .mbox .close:hover{border-color:var(--amber)} .foot{font-size:10.5px;color:var(--fog-2);margin-top:14px;line-height:1.55;border-top:1px solid var(--line);padding-top:12px} .tabbar{display:none} @media (max-width:1180px){.body{grid-template-columns:284px 1fr 348px}} @media (max-width:980px){ body{overflow:auto} .body{grid-template-columns:1fr;position:relative;flex:1;min-height:0} aside.panel,.stage,aside.right{display:none} aside.panel{border:0;border-bottom:1px solid var(--line)} aside.right{border:0} .view-filters aside.panel{display:block} .view-map .stage{display:block} .view-rank aside.right{display:block} .stage{height:100%} .tabbar{display:flex;flex:0 0 auto;border-top:1px solid var(--line);background:var(--ink-2)} .tabbar button{flex:1;background:none;border:0;color:var(--fog);padding:9px 4px 8px;display:flex;flex-direction:column;align-items:center;gap:3px;font-size:10px} .tabbar button svg{width:19px;height:19px} .tabbar button.on{color:var(--amber)} .search{min-width:0;flex:1} .top .seg{display:none} header.top{gap:10px;padding:9px 12px} .brand h1{font-size:14px} .maptop{flex-direction:column} .legend{max-width:180px} .pulse button span.bl{display:none} } @keyframes flow{to{stroke-dashoffset:-110}} .flow{animation:flow 3.2s linear infinite} @media (prefers-reduced-motion:reduce){.flow{animation:none}} </style> </head> <body> <div class="app" id="app"> <header class="top"> <div class="brand"> <div class="mark" aria-hidden="true"> <svg viewBox="0 0 24 24" fill="none"> <path d="M12 2 L20 7 V17 L12 22 L4 17 V7 Z" stroke="#eab34c" stroke-width="1.4" opacity=".9"/> <path d="M12 2 V22 M4 7 L20 17 M20 7 L4 17" stroke="#37b6a4" stroke-width="1" opacity=".55"/> <circle cx="12" cy="12" r="2.4" fill="#eab34c"/> </svg> </div> <div><h1>Meridian</h1><div class="sub">Sydney Property Alpha · v3</div></div> </div> <div class="seg" id="strategySeg"> <button data-strat="balanced" class="on">Balanced</button> <button data-strat="growth">Capital growth</button> <button data-strat="yield">Rental yield</button> </div> <div class="spacer"></div> <div class="search"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="11" cy="11" r="7"/><path d="M21 21l-4.3-4.3"/></svg> <input id="search" list="sublist" placeholder="Analyse an address or suburb…" autocomplete="off"/> <datalist id="sublist"></datalist> </div> </header> <div class="pulse" id="pulse"></div> <div class="body" id="bodyGrid"> <!-- LEFT --> <aside class="panel left"> <div class="sect" id="finSect"> <h2 class="click" data-collapse="finSect"><span class="idx">00</span> Deposit &amp; buying power <svg class="caret" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><path d="M6 9l6 6 6-6"/></svg></h2> <div class="sbody"> <div class="frow"> <div class="field"><label>Your deposit</label><div class="money"><input id="fDep" inputmode="numeric" placeholder="120000"></div></div> <div class="field"><label>Income (gross/yr)</label><div class="money"><input id="fInc" inputmode="numeric" placeholder="115000"></div></div> </div> <div class="frow"> <div class="field"><label>Partner income</label><div class="money"><input id="fInc2" inputmode="numeric" placeholder="0"></div></div> <div class="field"><label>Monthly living costs</label><div class="money"><input id="fExp" inputmode="numeric" placeholder="auto (HEM)"></div></div> </div> <div class="frow"> <div class="field"><label>Debt repayments /mo</label><div class="money"><input id="fDebt" inputmode="numeric" placeholder="0"></div></div> <div class="field"><label>Dependants</label> <select class="inp" id="fKids"><option value="0" selected>0</option><option value="1">1</option><option value="2">2</option><option value="3">3</option></select></div> </div> <div class="frow"> <div class="field"><label>Buyer type</label> <select class="inp" id="fWho"><option value="oo" selected>Owner-occupier</option><option value="inv">Investor</option></select></div> <div class="field"><label>First home buyer?</label> <select class="inp" id="fFhb"><option value="1">Yes</option><option value="0" selected>No</option></select></div> </div> <div class="field" id="rentField" style="display:none"><label>Expected rent /wk (80% counted)</label> <div class="money"><input id="fRent" inputmode="numeric" placeholder="650"></div></div> <div id="bpOut"></div> <div class="note" style="margin-top:10px">Serviceability at <b class="mono" id="assessNote"></b> (current variable + APRA 3% buffer), 30-yr P&amp;I, HEM-style expense floor, real NSW 2025–26 duty. Lender policies vary — treat as a tight central estimate.</div> </div> </div> <div class="sect"> <h2><span class="idx">01</span> Property brief</h2> <div class="field"><label>Dwelling type</label> <div class="chips" id="typeChips"> <button class="chip on" data-type="unit">Unit / Apt</button> <button class="chip" data-type="house">House</button> <button class="chip" data-type="townhouse">Townhouse</button> </div></div> <div class="field"><label>Bedrooms</label> <div class="chips" id="bedChips"> <button class="chip sm" data-bed="1">1</button><button class="chip sm on" data-bed="2">2</button> <button class="chip sm" data-bed="3">3</button><button class="chip sm" data-bed="4">4</button> <button class="chip sm" data-bed="5">5+</button></div></div> <div class="field"><label>Bathrooms</label> <div class="chips" id="bathChips"> <button class="chip sm on" data-bath="1">1</button><button class="chip sm" data-bath="2">2</button> <button class="chip sm" data-bath="3">3+</button></div></div> <div class="field"><label>Parking</label> <div class="chips" id="carChips"> <button class="chip sm" data-car="0">0</button><button class="chip sm on" data-car="1">1</button> <button class="chip sm" data-car="2">2</button><button class="chip sm" data-car="3">3+</button></div></div> <div class="field" id="landField" style="display:none"><label>Land / block</label> <select class="inp" id="landSel"> <option value="0.9">Small (&lt;350m²)</option><option value="1" selected>Average (~500m²)</option> <option value="1.15">Large (700m²+)</option><option value="1.32">Subdividable / dual-occ</option></select></div> <div class="field"><label>Condition</label> <select class="inp" id="condSel"> <option value="0.9">Original / dated</option><option value="1" selected>Good / liveable</option> <option value="1.1">Renovated</option><option value="1.18">New / near-new</option></select></div> <div class="field"><label>I commute to <span style="color:var(--fog-2)">(re-weights transport)</span></label> <select class="inp" id="workSel"> <option value="cbd" selected>Sydney CBD</option><option value="parra">Parramatta</option> <option value="macq">Macquarie Park</option><option value="liv">Liverpool</option> <option value="apt">Airport / Mascot</option></select></div> </div> <div class="sect"> <h2><span class="idx">02</span> Scoring weights <span class="hint" id="stratLabel">Balanced</span></h2> <div id="weights"></div> <button id="resetW" style="margin-top:6px;width:100%;background:var(--ink-3);border:1px solid var(--line);border-radius:8px;padding:7px;font-size:11px;color:var(--fog)">Reset to strategy defaults</button> </div> <div class="sect"> <h2><span class="idx">03</span> Budget filter</h2> <div class="field"><div class="money"><input id="budget" inputmode="numeric" placeholder="e.g. 750000"/></div></div> <label class="toggle" id="budgetTog"><span class="sw"></span><span class="tl">Dim suburbs over budget</span></label> <div class="note" id="budgetNote"></div> </div> <div class="sect"> <h2><span class="idx">04</span> Map layers</h2> <div id="layers"></div> </div> </aside> <!-- CENTER --> <section class="stage" id="stage"> <svg id="map" viewBox="0 0 1000 760" preserveAspectRatio="xMidYMid slice"></svg> <div class="maptop"> <div class="legend"> <h3>Investment score</h3> <div class="lgrad"></div> <div class="lgl"><span>40</span><span>60</span><span>80</span><span>95+</span></div> <div class="lgrow"><span class="swatch dash" style="border-color:var(--future)"></span>Future line</div> <div class="lgrow"><span class="swatch" style="border-color:var(--teal)"></span>Metro (open)</div> <div class="lgrow"><span class="swatch" style="border-color:var(--road)"></span>Motorway</div> </div> </div> <div class="mapctl"> <button id="zin" title="Zoom in">+</button> <button id="zout" title="Zoom out">−</button> <button id="zreset" title="Reset view">⌖</button> </div> <div class="scalebar">≈ 5 km<div class="bar"></div></div> <div class="credit">MERIDIAN · schematic cartography</div> <div class="cmptray hide" id="cmptray"></div> <div class="tip" id="tip"></div> </section> <!-- RIGHT --> <aside class="panel right"> <div id="rankView"> <div class="rhead"><h2>Ranked</h2> <select class="inp" id="regionSel" style="height:27px;width:auto;max-width:150px;font-size:11px;padding:0 24px 0 8px"></select> <span class="count" id="rcount"></span></div> <div class="rfilt"> <input id="rq" placeholder="Filter suburbs…"> <input id="minY" inputmode="decimal" placeholder="min yld %"> <input id="maxCm" inputmode="numeric" placeholder="max mins"> </div> <div class="rsort" id="rsort"> <button data-sort="score" class="on">Score</button> <button data-sort="fc">1-yr forecast</button> <button data-sort="value">Best value</button> <button data-sort="commute">Commute</button> <button data-sort="growth">Momentum</button> <button data-sort="future">Future transit</button> <button data-sort="yield">Yield</button> <button data-sort="gentr">Gentrifying</button> <button data-sort="price">Price ↑</button> </div> <div class="rlist" id="rlist"></div> </div> <div class="detail" id="detailView"> <div class="dhead"> <button class="back" id="backBtn"> <svg viewBox="0 0 24 24" width="13" height="13" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M15 18l-6-6 6-6"/></svg> Back to ranking</button> <div class="dtitle"> <div><div class="dn" id="dName"></div><div class="dr" id="dRegion"></div></div> <div class="dgrade"><div class="g" id="dGrade"></div><div class="gl">grade</div></div> </div> <div class="dscorebar"> <span class="num" id="dScoreNum"></span> <div class="track"><div class="f" id="dScoreFill"></div></div> <span class="rank" id="dRank"></span> </div> </div> <div class="dbody" id="dBody"></div> </div> </aside> </div> <nav class="tabbar" id="tabbar"> <button data-view="filters"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M7 12h10M10 18h4"/></svg>Brief</button> <button data-view="map" class="on"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 4 3 6v14l6-2 6 2 6-2V4l-6 2-6-2Z"/><path d="M9 4v14M15 6v14"/></svg>Map</button> <button data-view="rank"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h10M4 12h14M4 18h7"/><path d="M18 15l3 3-3 3"/></svg>Ranking</button> </nav> </div> <div class="modal" id="srcModal"> <div class="mbox"> <h2>Data &amp; sources</h2> <div class="md">Researched 3 July 2026 · calibration anchors for this build</div> <div id="srcList"></div> <div class="foot">Personal decision-support tool. Suburb-level figures are calibrated estimates anchored to the sources above and drift between refreshes of this file — always verify against live comparable sales and Revenue&nbsp;NSW before contracting, and treat outputs as general information, not licensed financial or credit advice.</div> <button class="close" id="srcClose">Close</button> </div> </div> <div class="modal" id="cmpModal"> <div class="mbox" style="max-width:720px"> <h2>Head-to-head</h2> <div class="md" id="cmpSub"></div> <div id="cmpTable" style="overflow-x:auto"></div> <button class="close" id="cmpClose">Close</button> </div> </div> <div class="modal" id="slModal"> <div class="mbox" style="max-width:680px"> <h2>My shortlist</h2> <div class="md">Actual properties you're tracking — scored against the model</div> <div class="frow" style="grid-template-columns:2fr 1.4fr 1fr .7fr auto;gap:8px;align-items:end"> <div class="field"><label>Address / label</label><div class="money" style="position:static"><input id="slAddr" style="padding-left:10px" placeholder="12/34 Example St"></div></div> <div class="field"><label>Suburb</label><input class="rfilt-like" id="slSub" list="sublist" placeholder="Marrickville" style="width:100%;height:36px;background:var(--ink-3);border:1px solid var(--line);border-radius:8px;padding:0 10px;font-size:12.5px;outline:0;color:var(--paper)"></div> <div class="field"><label>Asking</label><div class="money"><input id="slAsk" inputmode="numeric" placeholder="950000"></div></div> <div class="field"><label>Beds</label><select class="inp" id="slBed"><option>1</option><option selected>2</option><option>3</option><option>4</option><option>5</option></select></div> <button id="slAdd" style="height:36px;background:var(--amber);border:0;border-radius:8px;padding:0 16px;font-weight:600;font-size:12.5px;color:#191203">Add</button> </div> <div id="slList" style="margin-top:14px"></div> <button class="close" id="slClose">Close</button> </div> </div> <div class="modal" id="methModal"> <div class="mbox"> <h2>Methodology</h2> <div class="md">How Meridian scores, forecasts and calculates</div> <div id="methList" style="font-size:12.5px;color:var(--paper-dim);line-height:1.65"></div> <button class="close" id="methClose">Close</button> </div> </div> <script> "use strict"; /* ============================================================ MARKET CONSTANTS — researched 3 Jul 2026 ============================================================ */ const MKT={ asOf:"3 Jul 2026", cashRate:4.35, // RBA, after Jun-2026 hike varOO:6.10, varINV:6.45, // typical big-bank variable P&I (6–7% band) buffer:3.0, // APRA serviceability buffer sydHouse:1607046, sydUnit:903080, // Cotality medians (Feb 2026) juneQtr:-3.2, juneMonth:-1.2, // Cotality HVI Jun 2026, Sydney clearance:51, // approx auction clearance vacancy:1.4, rentYoY:7.4, rentG:3.5, // SQM: vacancy ~1.3-1.5% (first rise in 12mo); rents +7.4% YoY, moderating to ~2-5% banks:[["CBA",2.0],["NAB",1.3],["ANZ",-0.7],["Westpac",-3.0]], // 2026 Sydney/NSW dwelling f'casts fhbExempt:800000, fhbTaper:1000000, fhgCap:1500000, // First Home Guarantee price cap (Sydney) buyCosts:3000 // conveyancing + inspections allowance }; const SOURCES=[ ["Cotality (CoreLogic) Home Value Index, Jun 2026","Sydney −1.2% m/m, −3.2% June quarter — largest monthly fall since Dec 2022; buyers' market. Medians: house $1,607,046 · unit $903,080 (Feb 2026). Units currently outperforming houses."], ["Cotality / OpenAgent SA3 growth, May 2026","12-mo leaders: Penrith +11.7% ($1.11M) · St Marys +11.6% ($1.14M) · Campbelltown +10.6% · Mount Druitt +10.5% · Merrylands–Guildford +9.8% · Sutherland–Menai +9.2% ($1.64M). Affordable west/south-west corridors leading."], ["RBA & lenders, Jun 2026","Cash rate 4.35% after the June hike (was 4.10% in March). Big-bank variable P&I ≈ 6–7%. APRA +3% serviceability buffer; tighter lending rules from 1 Feb 2026."], ["Big-4 bank forecasts, Sydney dwellings 2026","CBA +2.0% · NAB +1.3% (NSW) · ANZ −0.7% · Westpac −3.0%. Wide spread = genuine rate-path uncertainty; used as scenario anchors here."], ["Revenue NSW, 2025–26 transfer duty","Tiered schedule (verified: $800k → $30,412; $1.2M → $48,412). Premium 7% marginal rate above $3,721,000. FHBAS: $0 duty ≤ $800k, linear taper to full duty at $1M (verified: $950k → $27,872)."], ["Home Guarantee Scheme (expanded Oct 2025)","5% deposit with no LMI, no income caps; Sydney/NSW-capitals price cap $1.5M. Stacks with NSW FHBAS. FHOG $10k applies to new builds ≤ $600k only."], ["SQM Research, mid-2026","Sydney vacancy ~1.5–2% (tight); asking house rents ~$1,100–1,200/wk; auction clearance ≈ 51%."], ["Sydney Metro / TfNSW project plans","Sydenham–Bankstown conversion H2 2026 · Western Sydney Airport line Dec 2027 (St Marys–Bradfield) · Metro West 2032 (Westmead–Hunter St, ~20 min Parramatta–CBD)."], ["SQM Research rental series, Apr–Jun 2026","Sydney vacancy ~1.3–1.5% — the first rise in 12 months, still far below the ~3% balanced benchmark. Combined Sydney rents +7.4% YoY; asking house rents ~$1,100–1,200/wk vs contracted median ~$800/wk (Domain). Forward guidance: rent growth moderating to ~2–5% through 2026."], ["Policy watch, May–Jun 2026","Federal Budget 2026 flagged investor tax changes (negative-gearing pullback scenarios modelled by SQM); NSW May-2025 rental reforms (no-grounds eviction ban) linked to landlord exits. Housing Accord supply target running materially behind with migration elevated — rentals stay structurally tight."] ]; /* ============================================================ PROJECTION ============================================================ */ const LNG0=150.58, LNG1=151.37, LATT=-33.56, LATB=-34.13, W=1000, H=760; const px=(lng)=>(lng-LNG0)/(LNG1-LNG0)*W; const py=(lat)=>(LATT-lat)/(LATT-LATB)*H; const P=(lat,lng)=>[px(lng),py(lat)]; /* ============================================================ SUBURB DATA — medians recalibrated to Jul-2026 research anchors ============================================================ */ const REGIONS={IW:"Inner West",SW:"South-West",CW:"Central West / Parramatta",WS:"Greater West",NS:"North Shore & Ryde",SS:"South & Shire",ES:"Eastern Suburbs",NW:"Hills & North-West",CY:"City Fringe",NB:"Northern Beaches",MC:"Macarthur & Camden",HW:"Hawkesbury"}; const S=(id,name,pc,reg,lat,lng,h,u,rh,ru,g,ft,tn,cm,ge,sc,zo,am,vac,sup,fld,lines,note)=>( {id,name,pc,reg,lat,lng,h,u,rh,ru,g,ft,tn,cm,ge,sc,zo,am,vac,sup,fld,lines,note}); const SUBURBS=[ S(1,"Marrickville",2204,"IW",-33.911,151.155,1750000,800000,920,620,76,88,74,66,86,62,72,86,1.7,42,38,["BK"],"The inner-west gentrification benchmark. The 2026 Bankstown metro conversion adds turn-up-and-go frequency to an already deep café/creative economy."), S(2,"Dulwich Hill",2203,"IW",-33.905,151.138,1880000,820000,900,610,72,86,72,60,80,64,60,78,1.6,38,34,["BK"],"Leafy and tightly held, with light rail plus the future metro. Strong owner-occupier demand caps supply."), S(3,"Hurlstone Park",2193,"IW",-33.906,151.128,1950000,790000,880,580,68,84,70,58,74,66,52,70,1.5,30,32,["BK"],"A small, quiet pocket where the metro upgrade lifts an already low-supply market."), S(4,"Canterbury",2193,"IW",-33.912,151.118,1650000,720000,820,560,74,85,70,62,72,58,74,72,1.9,50,46,["BK"],"Riverfront renewal plus metro. Watch the unit pipeline along the corridor."), S(5,"Campsie",2194,"IW",-33.911,151.103,1600000,640000,760,520,80,88,70,62,66,56,82,74,2.2,64,40,["BK"],"An earlier-stage gentrifier with major upzoning planned around the new metro station — high upside, higher supply risk."), S(6,"Belmore",2192,"SW",-33.919,151.089,1500000,620000,720,510,78,86,66,60,58,54,78,66,2.1,58,30,["BK"],"A value entry on the metro line, tracking the renewal ripple out from Campsie and Marrickville."), S(7,"Lakemba",2195,"SW",-33.920,151.075,1300000,520000,650,470,82,85,64,60,48,50,80,68,2.6,60,28,["BK"],"High yield and early-cycle. Metro plus affordability underpin ripple potential; still speculative."), S(8,"Wiley Park",2195,"SW",-33.921,151.064,1250000,500000,620,460,82,84,62,60,44,48,80,60,2.7,60,26,["BK"],"One of the cheapest metro-line entries in Sydney — genuinely early-stage."), S(9,"Punchbowl",2196,"SW",-33.928,151.056,1300000,540000,650,480,80,84,62,62,46,50,78,62,2.5,58,24,["BK"],"Affordable and car-friendly; the metro reshapes commute economics from 2026."), S(10,"Bankstown",2200,"SW",-33.917,151.035,1350000,560000,680,500,84,90,66,70,56,54,92,78,2.4,72,30,["BK"],"The metro terminus with CBD-scale upzoning — the biggest structural rezoning play here, but unit oversupply risk is real."), S(11,"St Peters",2044,"IW",-33.909,151.180,1700000,850000,900,640,72,70,80,72,82,58,64,80,1.6,44,48,["M1"],"Already on the metro edge; industrial-to-residential renewal continues. Some flight-path noise."), S(12,"Newtown",2042,"IW",-33.897,151.179,1900000,900000,950,680,64,68,84,66,84,60,50,94,1.4,32,30,[],"Peak-amenity inner west. Growth is stabilising as prices mature — a hold, not a breakout."), S(13,"Sydenham",2044,"IW",-33.914,151.167,1550000,780000,840,600,70,82,86,70,72,54,70,68,1.8,46,52,["M1","BK"],"The interchange node where City and Southwest metro meet. Flight-path noise caps some upside."), S(14,"Leichhardt",2040,"IW",-33.884,151.157,2000000,900000,980,660,62,62,74,64,78,62,48,86,1.4,28,26,[],"Established and tightly held — steady rather than explosive."), S(15,"Ashfield",2131,"IW",-33.887,151.125,1900000,700000,880,560,70,66,80,66,72,64,76,80,2.0,54,30,["T"],"A dense unit market on the main line — good yield with some supply overhang."), S(16,"Petersham",2049,"IW",-33.894,151.155,1850000,780000,900,600,64,64,78,62,80,70,50,82,1.4,26,28,[],"Fort Street catchment plus café culture — blue-chip inner west."), S(17,"Five Dock",2046,"IW",-33.867,151.128,2400000,950000,1050,680,80,92,58,68,70,66,66,80,1.3,34,24,["MW"],"Currently rail-poor: Metro West is transformational, delivering a first-ever fast CBD link by 2032."), S(18,"Burwood",2134,"IW",-33.877,151.104,2200000,780000,980,600,74,84,78,68,68,74,82,84,2.1,60,22,["MW","T"],"Burwood North metro plus high-rise upzoning, anchored by strong schools and a retail core."), S(19,"North Strathfield",2137,"IW",-33.858,151.088,2000000,850000,950,620,76,88,70,70,62,72,70,74,1.7,44,26,["MW","T"],"A Metro West station beside DFO/retail — middle-ring convenience with an infrastructure catalyst."), S(20,"Sydney Olympic Park",2127,"CW",-33.847,151.067,1900000,780000,900,640,70,86,64,72,58,50,88,72,2.8,82,58,["MW"],"Master-planned density plus Metro West — but a very large apartment pipeline means real oversupply risk."), S(21,"Parramatta",2150,"CW",-33.817,151.005,1520000,630000,720,540,84,94,80,82,72,64,94,88,2.5,78,40,["MW","T"],"Sydney's second CBD. Metro West cuts CBD trips to ~20 min; jobs and rezoning are strong, unit supply heavy."), S(22,"Westmead",2145,"CW",-33.807,150.986,1620000,650000,720,540,82,92,74,78,62,60,86,82,2.6,74,34,["MW","T"],"A health and innovation super-precinct with Metro West — jobs-anchored demand."), S(23,"Pyrmont",2009,"CY",-33.869,151.194,2200000,1150000,1100,820,68,86,82,58,70,48,72,86,2.2,56,40,["MW"],"Metro West plus Tech Central spillover, premium harbourside units."), S(24,"Rozelle",2039,"IW",-33.865,151.171,2300000,1000000,1080,700,72,84,66,76,76,58,64,82,1.5,40,34,["MW"],"The Bays metro plus Rozelle parklands and interchange — harbour-fringe renewal."), S(25,"Strathfield",2135,"IW",-33.873,151.095,2600000,780000,1050,600,66,70,82,74,58,86,78,82,2.0,58,22,["T"],"The elite school belt — land value underpinned by education demand."), S(26,"Homebush",2140,"IW",-33.865,151.083,2000000,720000,920,560,72,74,74,74,58,72,76,72,2.2,56,28,["T"],"Between metro and Olympic Park; schools plus convenience drive steady growth."), S(27,"St Marys",2760,"WS",-33.762,150.774,980000,600000,620,480,90,94,70,82,52,50,90,70,2.3,66,36,["WSA","T"],"+11.6% over the last year (Cotality) — the airport-metro interchange with huge upzoning. The structural western play, though the CBD commute stays long."), S(28,"Orchard Hills",2748,"WS",-33.795,150.755,1250000,0,680,0,84,92,40,74,44,46,88,48,2.0,58,30,["WSA"],"A greenfield metro precinct near the airport line — land-banking / house-and-land with limited comparable sales."), S(29,"Penrith",2750,"WS",-33.751,150.694,1080000,560000,600,470,88,70,66,80,56,58,80,84,2.2,58,54,["T"],"Sydney's #1 growth region over the last 12 months (+11.7%, Cotality May-26). Affordability plus the airport catalyse the whole corridor."), S(30,"Blacktown",2148,"WS",-33.771,150.907,1100000,560000,600,470,84,74,72,82,54,52,84,80,2.4,66,34,["T"],"A large affordable base with jobs and transport upgrades — the affordable belt is where growth is concentrating."), S(31,"Liverpool",2170,"WS",-33.920,150.923,1150000,560000,600,480,82,80,72,84,56,56,90,82,2.6,72,48,["T"],"Third-city ambitions plus a health precinct; airport-line spur potential."), S(32,"Cabramatta",2166,"WS",-33.895,150.936,1180000,520000,600,460,80,70,72,76,58,52,72,84,2.3,54,40,["T"],"A strong cultural retail economy — high yield, affordable entry, in the outperforming south-west belt."), S(33,"Auburn",2144,"WS",-33.849,151.033,1350000,560000,650,500,78,80,76,78,54,50,86,74,2.7,78,44,["T"],"Central-west location with heavy rezoning; unit oversupply is the key risk."), S(34,"Lidcombe",2141,"WS",-33.865,151.045,1500000,640000,720,540,76,82,78,78,56,58,82,72,2.5,70,30,["T"],"A rail junction near Olympic Park — convenient but a supply-heavy unit market."), S(35,"Chatswood",2067,"NS",-33.797,151.180,3500000,1100000,1300,780,62,74,90,72,62,84,80,92,2.1,50,20,["M1"],"A metro-plus-rail super-hub with top schools and major retail — premium and proven, but the top end is leading the current softening."), S(36,"Epping",2121,"NS",-33.772,151.082,2600000,780000,1000,600,68,80,86,72,58,88,82,78,2.3,64,22,["M1","T"],"Metro plus selective-school gravity keeps land value very well underpinned."), S(37,"Macquarie Park",2113,"NS",-33.777,151.126,2400000,850000,1050,680,70,80,86,70,56,70,84,80,2.8,76,24,["M1"],"University plus business park plus metro — unit-led, with a meaningful supply pipeline."), S(38,"Ryde",2112,"NS",-33.813,151.105,2300000,780000,980,600,68,74,74,74,58,74,76,78,2.2,58,26,[],"A central north-west position with steady family-driven demand."), S(39,"Hornsby",2077,"NS",-33.702,151.099,1900000,720000,880,560,70,70,80,74,56,80,74,82,2.0,52,28,["T"],"The upper-north-shore gateway — strong schools, rezoning and rail underpin."), S(40,"Hurstville",2220,"SS",-33.967,151.102,1900000,680000,880,560,72,68,80,76,60,76,84,86,2.3,64,26,["T"],"A major southern hub; a future Illawarra-line metro conversion is a live upside option."), S(41,"Rockdale",2216,"SS",-33.952,151.138,1700000,640000,820,540,74,70,78,78,60,60,82,78,2.4,66,50,["T"],"Near airport and beaches, rezoning-driven — but with some low-lying flood exposure."), S(42,"Kogarah",2217,"SS",-33.960,151.132,1800000,660000,850,550,72,70,80,76,58,72,80,80,2.2,60,34,["T"],"A hospital and education precinct — a solid all-round middle-ring pick."), S(43,"Sutherland",2232,"SS",-34.031,151.057,1580000,720000,850,580,76,66,74,76,56,72,70,80,1.9,48,28,["T"],"Shire gateway with rail and hospital; the Sutherland–Menai SA3 posted +9.2% over the last year."), S(44,"Cronulla",2230,"SS",-34.056,151.153,2800000,1100000,1200,820,62,58,66,64,62,70,54,88,1.7,38,44,["T"],"Beachside premium — lifestyle scarcity drives long-run growth, though the premium end is softest right now."), S(45,"Mascot",2020,"SS",-33.923,151.194,1900000,850000,950,700,68,76,82,80,60,50,84,74,3.0,84,46,["T"],"Airport-adjacent and CBD-close — convenient, but the most supply-saturated unit market here."), S(46,"Bondi Junction",2022,"ES",-33.891,151.247,3500000,1300000,1500,880,60,62,86,60,66,72,72,94,1.8,50,20,["T"],"A transport and retail interchange for the eastern beaches — blue-chip units; premium segment currently correcting."), S(47,"Randwick",2031,"ES",-33.914,151.241,3400000,1100000,1400,820,62,70,78,58,66,80,64,90,1.6,40,22,["L"],"Light rail plus hospital plus UNSW — deep, resilient demand."), S(48,"Maroubra",2035,"ES",-33.949,151.243,3000000,1000000,1300,760,64,58,66,58,64,64,60,84,1.7,42,28,[],"Beachside east at a relative discount to Bondi — lifestyle growth."), S(49,"Carlingford",2118,"NW",-33.783,151.049,2100000,760000,950,580,70,74,70,72,56,90,74,74,2.2,56,22,["L"],"James Ruse catchment gravity plus light rail — education-driven land value."), S(50,"Rouse Hill",2155,"NW",-33.687,150.916,1500000,680000,720,560,78,82,78,78,54,66,80,84,2.1,60,24,["M1"],"A Metro Northwest growth centre — family demand plus town-centre expansion."), S(51,"Crows Nest",2065,"NS",-33.826,151.203,3300000,1150000,1250,800,64,78,88,78,70,74,78,90,1.8,46,18,["M1"],"Metro opened 2024 and the TOD upzoning around the station is still repricing — premium north-shore village with a genuine structural catalyst."), S(52,"St Leonards",2065,"NS",-33.822,151.194,2900000,1050000,1150,760,62,74,88,78,62,70,82,82,2.4,66,16,["M1","T"],"Twin-station precinct with a major tower pipeline — convenience is elite, supply is the watch-out."), S(53,"Waterloo",2017,"CY",-33.899,151.207,1750000,850000,950,720,72,82,88,82,78,48,92,80,2.9,86,20,["M1"],"Metro open plus the Waterloo renewal precinct — big structural upzoning with equally big apartment supply."), S(54,"Zetland",2017,"CY",-33.906,151.208,1900000,900000,1000,740,68,68,78,80,74,46,84,84,3.0,88,26,[],"Green Square's core: hyper-convenient, deeply supplied — a yield-and-convenience play more than a scarcity play."), S(55,"Erskineville",2043,"IW",-33.902,151.186,1850000,880000,950,660,68,70,84,76,84,56,56,88,1.5,38,30,["T"],"Village-scale inner west that gentrified early and held — tightly supplied terraces near everything."), S(56,"Redfern",2016,"CY",-33.893,151.204,1950000,850000,980,700,70,74,88,82,82,50,74,86,1.9,56,18,["T"],"Tech Central spillover plus deep transit — the CBD-fringe conversion continues block by block."), S(57,"Wolli Creek",2205,"SS",-33.930,151.154,0,720000,0,620,70,66,84,80,60,46,80,74,3.1,88,55,["T"],"A pure apartment precinct at a rail junction — strong transit, heavy supply and river-flat flood exposure."), S(58,"Arncliffe",2205,"SS",-33.936,151.147,1650000,640000,800,560,74,68,78,78,62,52,78,70,2.4,62,52,["T"],"Airport-side value with rezoning along the Princes corridor; check flood mapping street by street."), S(59,"Kingsgrove",2208,"SS",-33.940,151.100,1700000,700000,820,560,74,64,74,74,58,60,66,68,1.9,44,30,["T"],"Quiet middle-ring value between the M8 and the rail line — steady family demand."), S(60,"Miranda",2228,"SS",-34.035,151.100,1750000,780000,880,620,72,60,72,70,60,66,74,90,1.8,50,22,["T"],"The Shire's retail heart — Westfield anchor, rail, and consistent downsizer and unit demand."), S(61,"Caringbah",2229,"SS",-34.042,151.121,1850000,800000,900,640,72,58,70,68,62,64,70,78,1.8,52,26,["T"],"Between Miranda and the bay — hospital, rail and a steady premium-lite market."), S(62,"Meadowbank",2114,"NS",-33.817,151.090,2100000,760000,900,600,70,70,78,76,60,62,76,72,2.6,72,44,["T"],"Riverside TAFE-and-apartments precinct — ferry plus rail, with a solid unit pipeline."), S(63,"West Ryde",2114,"NS",-33.807,151.089,2150000,720000,920,580,70,68,78,76,58,68,70,72,2.2,56,30,["T"],"Quiet rail suburb near the geographic centre of Sydney — schools and connectivity underpin."), S(64,"Granville",2142,"CW",-33.831,151.011,1250000,550000,640,490,80,78,80,80,60,52,84,72,2.5,68,42,["T"],"On Parramatta's doorstep with junction-station rail — the CBD-of-the-west ripple runs straight through here."), S(65,"Merrylands",2160,"CW",-33.837,150.990,1300000,560000,650,500,86,72,76,76,58,54,82,78,2.4,66,36,["T"],"Merrylands–Guildford posted +9.8% over the last year — town-centre renewal plus Parramatta spillover."), S(66,"Guildford",2161,"CW",-33.854,150.986,1250000,530000,630,480,84,70,74,74,54,50,76,66,2.3,58,38,["T"],"The cheaper half of the Merrylands–Guildford growth corridor — value with momentum."), S(67,"Toongabbie",2146,"CW",-33.789,150.951,1350000,600000,680,510,80,70,76,76,54,58,72,66,2.1,52,34,["T"],"Between Parramatta, Westmead and Blacktown — jobs gravity on three sides."), S(68,"Seven Hills",2147,"WS",-33.774,150.936,1250000,600000,660,510,82,70,76,78,52,54,74,70,2.1,54,30,["T"],"Industrial-jobs belt with rail and M2/M7 access — a dependable growth-belt workhorse."), S(69,"Mount Druitt",2770,"WS",-33.766,150.819,1000000,520000,580,460,88,72,72,76,46,46,80,74,2.2,56,32,["T"],"+10.5% over the last year — deep affordability, Westfield anchor, and airport-corridor adjacency."), S(70,"Quakers Hill",2763,"WS",-33.734,150.884,1250000,0,680,0,82,68,72,74,50,60,66,66,1.9,52,26,["T"],"Family-belt houses near the metro corridor — tight vacancy and steady owner-occupier depth."), S(71,"Schofields",2762,"NW",-33.704,150.871,1350000,700000,720,560,80,76,74,74,52,58,78,64,2.4,74,36,["T"],"Fast-growing station precinct beside the Tallawong metro terminus — new stock, new families."), S(72,"Marsden Park",2765,"NW",-33.679,150.837,1300000,0,700,0,80,72,46,66,48,52,82,70,2.5,80,40,[],"Master-planned business-park suburb — a mooted metro extension is the free option; car-dependent today."), S(73,"Leppington",2179,"SW",-33.954,150.806,1150000,640000,660,520,84,82,70,70,46,48,86,58,2.6,78,34,["T"],"Rail head of the aerotropolis frontier — heavy rezoning and a possible airport-line link keep the upside live."), S(74,"Campbelltown",2560,"SW",-34.065,150.814,1020000,560000,600,480,86,66,74,70,50,52,82,80,2.0,60,30,["T"],"+10.6% over the last year — Macarthur's capital with hospital, uni presence and genuine affordability."), S(75,"Fairfield",2165,"WS",-33.873,150.956,1150000,500000,600,460,82,64,74,72,52,48,74,74,2.2,54,44,["T"],"One of the strongest value pockets in the outperforming south-west — cash-flow friendly entry prices."), S(76,"Bondi",2026,"ES",-33.893,151.264,3400000,1150000,1400,800,60,56,73,58,63,64,54,88,1.5,36,30,["T"],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(77,"Bondi Beach",2026,"ES",-33.890,151.274,3570000,1164000,1440,810,60,56,64,58,63,64,54,88,1.5,36,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(78,"North Bondi",2026,"ES",-33.886,151.280,3740000,1179000,1480,810,61,58,64,60,65,66,55,90,1.6,39,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(79,"Tamarama",2026,"ES",-33.900,151.270,4420000,1236000,1640,840,61,58,64,60,65,66,55,90,1.6,39,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(80,"Bronte",2024,"ES",-33.904,151.263,3910000,1193000,1520,820,64,60,64,62,69,68,58,92,1.9,44,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(81,"Waverley",2024,"ES",-33.900,151.256,3230000,1136000,1360,790,63,59,64,61,67,67,57,91,1.8,42,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(82,"Clovelly",2031,"ES",-33.913,151.256,3740000,1179000,1480,810,63,59,64,61,67,67,57,91,1.8,42,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(83,"Coogee",2034,"ES",-33.919,151.254,3400000,1150000,1400,800,63,58,64,60,67,66,57,90,1.8,41,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(84,"South Coogee",2034,"ES",-33.933,151.253,3060000,1121000,1310,790,64,60,64,61,68,68,58,92,1.9,43,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(85,"Maroubra South",2035,"ES",-33.958,151.240,2788000,1098000,1240,780,63,59,64,61,67,67,57,91,1.8,42,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(86,"Malabar",2036,"ES",-33.963,151.248,2890000,1107000,1270,780,62,58,64,60,66,66,56,90,1.7,40,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(87,"Matraville",2036,"ES",-33.963,151.232,2720000,1092000,1220,780,61,57,64,59,65,65,55,89,1.6,38,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(88,"Chifley",2036,"ES",-33.968,151.240,2652000,1087000,1210,770,64,60,64,61,68,68,58,92,1.9,43,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(89,"Little Bay",2036,"ES",-33.980,151.243,2720000,1092000,1220,780,63,58,64,60,67,66,57,90,1.8,41,30,[],"Eastern-beaches lifestyle market — scarcity-driven long-run growth; the premium end is leading the current softening."), S(90,"Paddington",2021,"ES",-33.884,151.226,3800000,1200000,1450,820,59,61,70,61,71,71,49,91,1.6,34,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(91,"Woollahra",2025,"ES",-33.888,151.240,4370000,1245000,1580,840,58,60,70,60,69,70,48,90,1.5,32,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(92,"Double Bay",2028,"ES",-33.877,151.243,4750000,1275000,1660,850,59,61,70,61,71,71,49,91,1.6,34,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(93,"Rose Bay",2029,"ES",-33.870,151.270,4560000,1260000,1620,840,62,64,70,64,75,74,52,94,1.9,40,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(94,"Vaucluse",2030,"ES",-33.855,151.278,5700000,1350000,1850,880,60,62,70,62,72,72,50,92,1.7,36,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(95,"Dover Heights",2030,"ES",-33.870,151.280,4560000,1260000,1620,840,60,62,70,62,72,72,50,92,1.7,36,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(96,"Bellevue Hill",2023,"ES",-33.880,151.250,5130000,1305000,1740,860,59,62,70,62,71,72,49,92,1.6,35,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(97,"Darling Point",2027,"ES",-33.870,151.236,4560000,1260000,1620,840,60,62,70,62,72,72,50,92,1.7,36,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(98,"Edgecliff",2027,"ES",-33.877,151.236,3610000,1185000,1410,810,62,64,79,63,74,74,52,94,1.9,39,30,["T"],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(99,"Queens Park",2022,"ES",-33.900,151.247,3610000,1185000,1410,810,62,64,70,64,75,74,52,94,1.9,40,30,[],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(100,"Kensington",2033,"ES",-33.910,151.222,3116000,1146000,1290,800,62,64,76,63,74,74,52,94,1.9,39,30,["L"],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(101,"Kingsford",2032,"ES",-33.924,151.227,2964000,1134000,1250,790,62,64,76,64,75,74,52,94,1.9,40,30,["L"],"Blue-chip inner east — tightly held, prestige-led, defensive rather than explosive."), S(102,"Surry Hills",2010,"CY",-33.886,151.212,2000000,950000,1000,730,68,76,91,81,82,52,74,94,2.4,65,30,["T"],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(103,"Darlinghurst",2010,"CY",-33.879,151.219,2000000,950000,1000,730,66,74,82,80,80,50,72,92,2.2,62,30,[],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(104,"Potts Point",2011,"CY",-33.870,151.225,1900000,938000,970,720,64,72,91,78,77,48,70,90,2.0,58,30,["T"],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(105,"Elizabeth Bay",2011,"CY",-33.872,151.228,1900000,938000,970,720,67,74,82,80,81,50,73,92,2.2,63,30,[],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(106,"Woolloomooloo",2011,"CY",-33.870,151.219,1800000,926000,940,720,65,74,82,80,79,50,71,92,2.2,61,30,[],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(107,"Chippendale",2008,"CY",-33.887,151.198,1900000,938000,970,720,66,74,91,80,80,50,72,92,2.2,62,30,["T"],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(108,"Ultimo",2007,"CY",-33.879,151.197,1700000,914000,910,710,67,74,88,80,81,50,73,92,2.2,63,30,["L"],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(109,"Alexandria",2015,"CY",-33.902,151.196,1900000,938000,970,720,67,75,82,81,81,51,73,93,2.3,64,30,[],"City-fringe terrace-and-apartment market — peak walkability and deep rental demand."), S(110,"Balmain",2041,"IW",-33.858,151.180,2242000,882000,1030,650,67,72,72,68,81,60,57,86,1.6,37,30,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(111,"Birchgrove",2041,"IW",-33.851,151.182,2340000,892000,1060,660,69,72,72,68,83,60,59,86,1.7,39,31,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(112,"Lilyfield",2040,"IW",-33.872,151.163,2048000,861000,980,640,67,71,78,67,81,59,57,85,1.5,36,30,["L"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(113,"Annandale",2038,"IW",-33.881,151.171,2048000,861000,980,640,69,73,78,69,83,61,59,87,1.7,40,32,["L"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(114,"Glebe",2037,"IW",-33.879,151.186,2048000,861000,980,640,69,73,78,69,83,61,59,87,1.7,40,32,["L"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(115,"Forest Lodge",2037,"IW",-33.881,151.180,1950000,850000,950,640,70,74,78,70,85,62,60,88,1.8,42,34,["L"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(116,"Camperdown",2050,"IW",-33.890,151.177,1852000,839000,920,640,67,71,72,67,81,59,57,85,1.5,36,30,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(117,"Stanmore",2048,"IW",-33.894,151.164,1911000,846000,940,640,70,74,81,70,85,62,60,88,1.8,42,34,["T"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(118,"Enmore",2042,"IW",-33.900,151.170,1911000,846000,940,640,69,73,72,69,83,61,59,87,1.7,40,32,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(119,"Lewisham",2049,"IW",-33.895,151.146,1852000,839000,920,640,70,74,81,70,85,62,60,88,1.8,42,34,["T"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(120,"Summer Hill",2130,"IW",-33.891,151.138,1852000,839000,920,640,66,70,81,67,80,58,56,84,1.5,35,30,["T"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(121,"Haberfield",2045,"IW",-33.882,151.141,2048000,861000,980,640,66,70,72,67,80,58,56,84,1.5,35,30,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(122,"Croydon",2132,"IW",-33.883,151.115,1755000,829000,890,630,70,74,81,69,84,62,60,88,1.8,41,33,["T"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(123,"Enfield",2136,"IW",-33.888,151.093,1658000,818000,860,630,70,74,72,70,85,62,60,88,1.8,42,34,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(124,"Drummoyne",2047,"IW",-33.852,151.155,2048000,861000,980,640,70,74,72,70,85,62,60,88,1.8,42,34,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(125,"Abbotsford",2046,"IW",-33.850,151.129,1950000,850000,950,640,69,73,72,69,83,61,59,87,1.7,40,32,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(126,"Mortlake",2137,"IW",-33.845,151.115,1852000,839000,920,640,67,71,72,67,81,59,57,85,1.5,36,30,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(127,"Concord",2137,"IW",-33.860,151.103,1950000,850000,950,640,66,70,72,66,79,58,56,84,1.4,34,30,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(128,"Concord West",2138,"IW",-33.848,151.086,1852000,839000,920,640,66,70,81,66,79,58,56,84,1.4,34,30,["T"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(129,"Tempe",2044,"IW",-33.923,151.160,1599000,812000,840,620,69,73,81,69,83,61,59,87,1.7,40,60,["T"],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(130,"Earlwood",2206,"IW",-33.920,151.125,1716000,824000,880,630,67,71,72,67,81,59,57,85,1.5,36,44,[],"Established inner west — gentrified early, tightly supplied, consistent owner-occupier depth."), S(131,"Belfield",2191,"SW",-33.902,151.088,1425000,592000,680,500,79,84,60,62,55,52,79,68,2.2,57,31,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(132,"Ashbury",2193,"SW",-33.898,151.120,1530000,603000,710,500,80,84,60,62,56,52,80,68,2.3,58,32,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(133,"Croydon Park",2133,"SW",-33.897,151.103,1470000,597000,690,500,80,84,60,62,56,52,80,68,2.3,58,32,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(134,"Strathfield South",2136,"SW",-33.893,151.078,1380000,588000,670,490,78,82,60,60,53,50,78,66,2.1,54,30,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(135,"Homebush West",2140,"SW",-33.863,151.069,1275000,578000,630,490,79,83,69,61,55,51,79,67,2.2,56,30,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(136,"Silverwater",2128,"CW",-33.833,151.048,1232000,578000,620,500,78,78,70,74,55,54,80,72,2.2,62,50,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(137,"Newington",2127,"SS",-33.837,151.056,1758000,790000,890,660,67,72,74,78,59,50,79,76,2.8,77,39,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(138,"Wentworth Point",2127,"SS",-33.830,151.075,1443000,756000,790,640,68,72,74,78,60,50,80,76,2.8,78,40,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(139,"Rhodes",2138,"SS",-33.828,151.086,1572000,770000,830,650,70,74,83,79,62,52,82,78,2.9,81,43,["T"],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(140,"Greenacre",2190,"SW",-33.900,151.056,1425000,592000,680,500,82,86,60,63,58,54,82,70,2.4,61,35,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(141,"Roselands",2196,"SW",-33.935,151.070,1350000,585000,660,490,82,86,60,63,58,54,82,70,2.4,61,35,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(142,"Yagoona",2199,"SW",-33.905,151.020,1350000,585000,660,490,79,84,69,62,55,52,79,68,2.2,57,31,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(143,"Birrong",2143,"SW",-33.893,151.017,1275000,578000,630,490,80,84,69,62,56,52,80,68,2.3,58,32,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(144,"Regents Park",2143,"SW",-33.885,151.025,1275000,578000,630,490,79,83,69,61,55,51,79,67,2.2,56,30,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(145,"Berala",2141,"SW",-33.872,151.032,1275000,578000,630,490,81,84,69,62,57,52,81,68,2.3,59,33,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(146,"Sefton",2162,"SW",-33.887,151.010,1230000,573000,620,490,80,84,69,62,56,52,80,68,2.3,58,32,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(147,"Chester Hill",2162,"SW",-33.880,150.997,1275000,578000,630,490,79,83,69,61,55,51,79,67,2.2,56,30,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(148,"Bass Hill",2197,"SW",-33.898,150.995,1275000,578000,630,490,81,84,60,62,57,52,81,68,2.3,59,33,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(149,"Georges Hall",2198,"SW",-33.908,150.984,1350000,585000,660,490,78,82,60,61,54,50,78,66,2.1,55,30,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(150,"Condell Park",2200,"SW",-33.925,150.990,1320000,582000,650,490,79,84,60,62,55,52,79,68,2.2,57,31,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(151,"Revesby",2212,"SW",-33.950,151.015,1425000,592000,680,500,81,85,69,63,57,53,81,69,2.4,60,34,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(152,"Padstow",2211,"SW",-33.955,151.030,1425000,592000,680,500,81,85,69,63,57,53,81,69,2.4,60,34,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(153,"Panania",2213,"SW",-33.958,150.998,1350000,585000,660,490,82,86,69,64,59,54,82,70,2.5,62,36,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(154,"East Hills",2213,"SW",-33.963,150.985,1275000,578000,630,490,82,86,69,63,58,54,82,70,2.4,61,55,["T"],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(155,"Milperra",2214,"SW",-33.940,150.975,1275000,578000,630,490,79,84,60,62,55,52,79,68,2.2,57,70,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(156,"Picnic Point",2213,"SW",-33.970,151.000,1425000,592000,680,500,80,84,60,62,56,52,80,68,2.3,58,32,[],"Canterbury–Bankstown corridor — the 2026 metro conversion plus corridor upzoning is the structural story."), S(157,"Harris Park",2150,"CW",-33.823,151.008,1232000,578000,620,500,78,78,79,75,56,54,80,72,2.2,63,33,["T"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(158,"Rosehill",2142,"CW",-33.820,151.020,1232000,578000,620,500,81,80,79,76,59,56,83,74,2.4,67,37,["T"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(159,"Rydalmere",2116,"CW",-33.813,151.028,1378000,592000,660,510,82,82,76,78,61,58,84,76,2.6,70,40,["L"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(160,"Ermington",2115,"CW",-33.815,151.055,1450000,600000,680,510,78,78,70,74,55,54,80,72,2.2,62,32,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(161,"Melrose Park",2114,"CW",-33.815,151.072,1305000,585000,640,500,79,80,70,76,57,56,81,74,2.4,65,35,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(162,"Dundas",2117,"CW",-33.800,151.032,1378000,592000,660,510,82,82,76,77,60,58,84,76,2.5,69,39,["L"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(163,"Telopea",2117,"CW",-33.795,151.037,1305000,585000,640,500,81,81,76,77,59,57,83,75,2.5,68,38,["L"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(164,"Oatlands",2117,"CW",-33.797,151.020,1522000,608000,700,510,78,78,70,75,56,54,80,72,2.2,63,33,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(165,"North Parramatta",2151,"CW",-33.790,151.005,1378000,592000,660,510,81,81,70,77,59,57,83,75,2.5,68,38,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(166,"Northmead",2152,"CW",-33.780,150.990,1378000,592000,660,510,81,80,70,76,59,56,83,74,2.4,67,37,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(167,"Winston Hills",2153,"CW",-33.777,150.977,1421000,597000,670,510,81,81,70,77,59,57,83,75,2.5,68,38,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(168,"Wentworthville",2145,"CW",-33.807,150.972,1305000,585000,640,500,78,78,79,75,56,54,80,72,2.2,63,33,["T"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(169,"Pendle Hill",2145,"CW",-33.800,150.955,1276000,582000,630,500,79,80,79,76,57,56,81,74,2.4,65,35,["T"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(170,"Girraween",2145,"CW",-33.800,150.940,1305000,585000,640,500,78,78,79,75,56,54,80,72,2.2,63,33,["T"],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(171,"Greystanes",2145,"CW",-33.822,150.946,1378000,592000,660,510,79,80,70,76,57,56,81,74,2.4,65,35,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(172,"Pemulwuy",2145,"CW",-33.810,150.920,1421000,597000,670,510,81,81,70,77,59,57,83,75,2.5,68,38,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(173,"South Granville",2142,"CW",-33.848,151.010,1232000,578000,620,500,79,80,70,76,57,56,81,74,2.4,65,35,[],"Parramatta ring — second-CBD jobs gravity, heavy rezoning, strong momentum in the affordable belt."), S(174,"Lalor Park",2147,"WS",-33.760,150.930,978000,539000,560,460,86,72,66,77,52,54,78,70,2.2,59,35,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(175,"Kings Langley",2147,"WS",-33.750,150.940,1127000,557000,610,470,86,72,66,77,52,54,78,70,2.2,59,35,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(176,"Prospect",2148,"WS",-33.795,150.910,1035000,546000,580,460,82,68,66,74,47,50,74,66,1.9,52,30,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(177,"Doonside",2767,"WS",-33.765,150.870,978000,539000,560,460,84,70,75,76,50,52,76,68,2.1,56,32,["T"],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(178,"Woodcroft",2767,"WS",-33.755,150.885,1092000,553000,600,470,82,68,66,75,48,50,74,66,2.0,53,30,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(179,"Rooty Hill",2766,"WS",-33.770,150.840,978000,539000,560,460,82,68,75,74,47,50,74,66,1.9,52,30,["T"],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(180,"Plumpton",2761,"WS",-33.755,150.840,1012000,543000,570,460,83,70,66,76,49,52,75,68,2.1,55,31,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(181,"Minchinbury",2770,"WS",-33.790,150.830,1058000,549000,590,460,82,68,66,74,47,50,74,66,1.9,52,30,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(182,"Whalan",2770,"WS",-33.758,150.800,862000,525000,520,450,85,71,66,77,51,53,77,69,2.2,58,34,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(183,"Lethbridge Park",2770,"WS",-33.742,150.800,840000,522000,510,450,86,72,66,77,52,54,78,70,2.2,59,35,[],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(184,"Marayong",2148,"WS",-33.748,150.895,978000,539000,560,460,85,70,75,76,51,52,77,68,2.1,57,33,["T"],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(185,"Erskine Park",2759,"WS",-33.815,150.795,998000,543000,580,460,86,74,60,75,48,50,78,68,2.1,55,43,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(186,"St Clair",2759,"WS",-33.800,150.785,998000,543000,580,460,90,78,60,78,53,54,82,72,2.4,62,50,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(187,"Colyton",2760,"WS",-33.782,150.795,892000,529000,540,450,89,76,60,76,51,52,81,70,2.2,59,47,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(188,"Oxley Park",2760,"WS",-33.772,150.795,872000,527000,540,450,90,78,60,78,53,54,82,72,2.4,62,50,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(189,"North St Marys",2760,"WS",-33.748,150.780,861000,525000,530,450,90,78,60,78,53,54,82,72,2.4,62,50,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(190,"Werrington",2747,"WS",-33.758,150.750,892000,529000,540,450,90,78,69,78,53,54,82,72,2.4,62,50,["T"],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(191,"Cambridge Park",2747,"WS",-33.745,150.720,892000,529000,540,450,87,75,60,75,49,51,79,69,2.1,56,44,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(192,"Kingswood",2747,"WS",-33.752,150.722,892000,529000,540,450,89,76,69,76,51,52,81,70,2.2,59,47,["T"],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(193,"Claremont Meadows",2747,"WS",-33.770,150.755,945000,536000,560,450,87,76,60,76,49,52,79,70,2.2,57,45,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(194,"Caddens",2747,"WS",-33.772,150.740,998000,543000,580,460,89,77,60,77,51,53,81,71,2.3,60,48,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(195,"Jordan Springs",2747,"WS",-33.720,150.720,998000,543000,580,460,89,77,60,77,51,53,81,71,2.3,60,48,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(196,"Ropes Crossing",2760,"WS",-33.725,150.775,945000,536000,560,450,87,75,60,75,49,51,79,69,2.1,56,44,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(197,"Emu Plains",2750,"WS",-33.750,150.655,945000,536000,560,450,87,75,69,75,49,51,79,69,2.1,56,60,["T"],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(198,"South Penrith",2750,"WS",-33.770,150.695,966000,539000,570,450,90,78,60,77,52,54,82,72,2.4,61,49,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(199,"Glenmore Park",2745,"WS",-33.790,150.670,1050000,550000,600,460,87,75,60,75,49,51,79,69,2.1,56,44,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(200,"Cranebrook",2749,"WS",-33.720,150.710,924000,534000,560,450,89,76,60,76,51,52,81,70,2.2,59,55,[],"Penrith corridor — Sydney's strongest 12-month growth region, catalysed by the airport build-out."), S(201,"Canley Vale",2166,"WS",-33.888,150.945,1035000,507000,570,460,82,72,75,74,52,50,78,72,2.3,58,42,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(202,"Canley Heights",2166,"WS",-33.892,150.925,1035000,507000,570,460,81,72,66,74,51,50,77,72,2.2,57,41,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(203,"Fairfield West",2165,"WS",-33.868,150.920,1012000,504000,560,460,84,74,66,75,54,52,80,74,2.4,61,45,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(204,"Fairfield Heights",2165,"WS",-33.862,150.940,1012000,504000,560,460,81,71,66,73,51,49,77,71,2.2,56,40,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(205,"Villawood",2163,"WS",-33.885,150.975,920000,494000,530,450,81,71,75,73,51,49,77,71,2.2,56,40,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(206,"Carramar",2163,"WS",-33.885,150.960,943000,497000,540,450,84,74,75,75,54,52,80,74,2.4,61,45,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(207,"Lansvale",2166,"WS",-33.900,150.950,978000,500000,550,450,81,71,66,73,51,49,77,71,2.2,56,70,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(208,"Cabramatta West",2166,"WS",-33.895,150.915,1012000,504000,560,460,84,74,66,75,54,52,80,74,2.4,61,45,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(209,"Bonnyrigg",2177,"WS",-33.890,150.890,1035000,507000,570,460,84,74,66,75,54,52,80,74,2.4,61,45,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(210,"St Johns Park",2176,"WS",-33.882,150.900,1092000,514000,590,460,84,74,66,75,54,52,80,74,2.4,61,45,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(211,"Prairiewood",2176,"WS",-33.868,150.900,1092000,514000,590,460,83,73,66,75,53,51,79,73,2.4,60,44,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(212,"Bossley Park",2176,"WS",-33.862,150.883,1150000,520000,610,460,83,72,66,74,53,50,79,72,2.3,59,43,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(213,"Warwick Farm",2170,"WS",-33.913,150.935,862000,488000,510,450,80,70,75,72,49,48,76,70,2.1,54,65,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(214,"Chipping Norton",2170,"WS",-33.925,150.955,1127000,517000,600,460,84,74,66,76,55,52,80,74,2.5,62,60,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(215,"Moorebank",2170,"WS",-33.940,150.940,1127000,517000,600,460,84,74,66,76,55,52,80,74,2.5,62,50,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(216,"Holsworthy",2173,"WS",-33.952,150.955,1058000,510000,580,460,83,73,75,75,53,51,79,73,2.4,60,44,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(217,"Wattle Grove",2173,"WS",-33.945,150.945,1150000,520000,610,460,84,74,66,76,55,52,80,74,2.5,62,46,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(218,"Casula",2170,"WS",-33.950,150.910,1092000,514000,590,460,84,74,75,76,55,52,80,74,2.5,62,46,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(219,"Prestons",2170,"WS",-33.940,150.870,1127000,517000,600,460,81,71,66,73,51,49,77,71,2.2,56,40,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(220,"Lurnea",2170,"WS",-33.935,150.895,978000,500000,550,450,81,72,66,74,51,50,77,72,2.2,57,41,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(221,"Green Valley",2168,"WS",-33.905,150.865,920000,494000,530,450,80,70,66,73,50,48,76,70,2.1,55,39,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(222,"Hinchinbrook",2168,"WS",-33.918,150.860,1035000,507000,570,460,82,72,66,74,52,50,78,72,2.3,58,42,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(223,"Hoxton Park",2171,"WS",-33.930,150.850,1092000,514000,590,460,80,70,66,72,49,48,76,70,2.1,54,38,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(224,"West Hoxton",2171,"WS",-33.925,150.830,1150000,520000,610,460,82,72,66,74,52,50,78,72,2.3,58,42,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(225,"Middleton Grange",2171,"WS",-33.918,150.840,1150000,520000,610,460,84,74,66,75,54,52,80,74,2.4,61,45,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(226,"Austral",2179,"WS",-33.930,150.810,1150000,520000,610,460,81,72,66,74,51,50,77,72,2.2,57,41,[],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(227,"Edmondson Park",2174,"WS",-33.970,150.860,1208000,526000,630,470,80,70,75,72,49,48,76,70,2.1,54,38,["T"],"Liverpool–Fairfield belt — cash-flow friendly entries in the outperforming south-west."), S(228,"Glenfield",2167,"MC",-33.968,150.890,950000,543000,570,470,87,66,71,68,49,50,81,70,2.0,63,33,["T"],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(229,"Macquarie Fields",2564,"MC",-33.985,150.880,850000,529000,540,460,85,66,71,68,47,50,79,70,1.9,61,31,["T"],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(230,"Ingleburn",2565,"MC",-34.000,150.865,950000,543000,570,470,88,68,71,70,51,52,82,72,2.2,66,36,["T"],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(231,"Minto",2566,"MC",-34.025,150.845,880000,534000,550,460,85,65,71,67,47,49,79,69,1.9,60,30,["T"],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(232,"Leumeah",2560,"MC",-34.052,150.838,900000,536000,550,460,84,64,71,66,45,48,78,68,1.8,58,30,["T"],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(233,"Bradbury",2560,"MC",-34.083,150.815,900000,536000,550,460,85,66,62,68,47,50,79,70,1.9,61,31,[],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(234,"Ambarvale",2560,"MC",-34.088,150.795,850000,529000,540,460,84,64,62,67,46,48,78,68,1.9,59,30,[],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(235,"Camden",2570,"MC",-34.054,150.696,1000000,550000,590,470,86,66,62,68,48,50,80,70,2.0,62,65,[],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(236,"Narellan",2567,"MC",-34.040,150.732,1000000,550000,590,470,86,66,62,68,48,50,80,70,2.0,62,32,[],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(237,"Mount Annan",2567,"MC",-34.060,150.760,1000000,550000,590,470,84,64,62,67,46,48,78,68,1.9,59,30,[],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(238,"Harrington Park",2567,"MC",-34.030,150.735,1100000,564000,620,480,87,66,62,68,49,50,81,70,2.0,63,33,[],"Macarthur — +10%-class annual growth on genuine affordability; infrastructure is catching up to population."), S(239,"Oran Park",2570,"MC",-34.005,150.740,1050000,600000,620,500,84,70,34,60,46,52,82,62,2.4,82,30,[],"Camden greenfield — master-planned land-and-house country; supply pipeline is the main brake on growth."), S(240,"Gregory Hills",2557,"MC",-34.020,150.770,1050000,600000,620,500,83,69,34,59,45,51,81,61,2.3,80,30,[],"Camden greenfield — master-planned land-and-house country; supply pipeline is the main brake on growth."), S(241,"Gledswood Hills",2557,"MC",-34.005,150.775,1102000,608000,640,500,83,70,34,60,45,52,81,62,2.4,81,30,[],"Camden greenfield — master-planned land-and-house country; supply pipeline is the main brake on growth."), S(242,"Spring Farm",2570,"MC",-34.070,150.715,998000,592000,600,500,83,69,34,59,45,51,81,61,2.3,80,55,[],"Camden greenfield — master-planned land-and-house country; supply pipeline is the main brake on growth."), S(243,"Kirribilli",2061,"NS",-33.848,151.216,3740000,1128000,1320,790,64,74,89,78,67,82,64,90,2.1,50,30,["T"],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(244,"Milsons Point",2061,"NS",-33.846,151.212,3400000,1100000,1250,780,62,72,89,76,64,80,62,88,1.9,46,30,["T"],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(245,"North Sydney",2060,"NS",-33.839,151.207,3400000,1100000,1250,780,64,74,92,78,67,82,64,90,2.1,50,30,["M1","T"],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(246,"Waverton",2060,"NS",-33.838,151.198,3570000,1114000,1290,790,60,70,89,74,61,78,60,86,1.7,42,30,["T"],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(247,"Wollstonecraft",2065,"NS",-33.831,151.191,3570000,1114000,1290,790,60,70,89,75,62,78,60,86,1.8,43,30,["T"],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(248,"Neutral Bay",2089,"NS",-33.832,151.220,3570000,1114000,1290,790,63,73,80,77,65,81,63,89,2.0,48,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(249,"Cremorne",2090,"NS",-33.828,151.227,3740000,1128000,1320,790,64,74,80,78,67,82,64,90,2.1,50,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(250,"Mosman",2088,"NS",-33.828,151.244,4420000,1182000,1460,810,62,72,80,76,64,80,62,88,1.9,46,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(251,"Cammeray",2062,"NS",-33.822,151.212,3570000,1114000,1290,790,62,72,80,76,64,80,62,88,1.9,46,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(252,"Northbridge",2063,"NS",-33.813,151.216,4080000,1155000,1390,800,64,74,80,77,66,82,64,90,2.0,49,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(253,"Naremburn",2065,"NS",-33.818,151.200,3400000,1100000,1250,780,61,71,80,75,63,79,61,87,1.8,44,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(254,"Artarmon",2064,"NS",-33.810,151.185,3230000,1086000,1210,770,61,71,89,75,63,79,61,87,1.8,44,30,["T"],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(255,"Willoughby",2068,"NS",-33.805,151.200,3570000,1114000,1290,790,61,71,80,75,63,79,61,87,1.8,44,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(256,"Castlecrag",2068,"NS",-33.798,151.222,4250000,1169000,1430,810,60,70,80,75,62,78,60,86,1.8,43,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(257,"Lane Cove",2066,"NS",-33.815,151.170,3400000,1100000,1250,780,63,72,80,76,65,80,63,88,1.9,47,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(258,"Lane Cove North",2066,"NS",-33.805,151.165,3230000,1086000,1210,770,60,70,80,74,61,78,60,86,1.7,42,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(259,"Greenwich",2065,"NS",-33.830,151.183,3910000,1141000,1360,800,62,72,80,76,64,80,62,88,1.9,46,30,[],"Lower North Shore — metro-enhanced blue chip; proven, defensive, and school-anchored."), S(260,"Roseville",2069,"NS",-33.783,151.180,2940000,861000,1130,640,66,66,83,73,58,88,66,76,2.0,47,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(261,"Lindfield",2070,"NS",-33.775,151.170,2940000,861000,1130,640,66,66,83,74,59,88,66,76,2.0,48,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(262,"Killara",2071,"NS",-33.766,151.162,3080000,871000,1160,650,62,62,83,70,53,84,62,72,1.6,40,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(263,"Gordon",2072,"NS",-33.756,151.154,2800000,850000,1100,640,64,64,83,72,56,86,64,74,1.8,44,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(264,"Pymble",2073,"NS",-33.744,151.140,2940000,861000,1130,640,63,64,83,72,55,86,63,74,1.8,43,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(265,"West Pymble",2073,"NS",-33.762,151.130,2660000,839000,1070,640,65,64,74,72,57,86,65,74,1.9,45,30,[],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(266,"Turramurra",2074,"NS",-33.733,151.128,2800000,850000,1100,640,65,64,83,72,57,86,65,74,1.9,45,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(267,"Wahroonga",2076,"NS",-33.718,151.117,2940000,861000,1130,640,64,64,83,72,56,86,64,74,1.8,44,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(268,"Waitara",2077,"NS",-33.710,151.103,2240000,808000,960,620,66,66,83,73,58,88,66,76,2.0,47,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(269,"Normanhurst",2076,"NS",-33.720,151.098,2520000,829000,1030,630,65,65,83,73,57,87,65,75,1.9,46,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(270,"Thornleigh",2120,"NS",-33.732,151.078,2660000,839000,1070,640,62,62,83,70,53,84,62,72,1.6,40,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(271,"Pennant Hills",2120,"NS",-33.738,151.072,2660000,839000,1070,640,63,64,83,72,55,86,63,74,1.8,43,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(272,"Beecroft",2119,"NS",-33.750,151.065,2940000,861000,1130,640,65,65,83,73,57,87,65,75,1.9,46,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(273,"St Ives",2075,"NS",-33.730,151.160,2940000,861000,1130,640,66,66,74,73,58,88,66,76,2.0,47,30,[],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(274,"Mount Colah",2079,"NS",-33.665,151.115,2380000,818000,1000,630,66,66,83,74,59,88,66,76,2.0,48,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(275,"Asquith",2077,"NS",-33.688,151.108,2380000,818000,1000,630,63,63,83,71,55,85,63,73,1.7,42,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(276,"Berowra",2081,"NS",-33.620,151.150,2240000,808000,960,620,62,62,83,71,54,84,62,72,1.6,41,30,["T"],"Upper North Shore — selective-school gravity and leafy land value; slow-burn compounder."), S(277,"North Ryde",2113,"NS",-33.796,151.124,2300000,780000,980,600,66,74,86,72,55,74,70,76,2.1,56,30,["M1"],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(278,"Gladesville",2111,"NS",-33.833,151.128,2415000,790000,1010,600,70,78,74,75,60,78,74,80,2.4,63,30,[],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(279,"Hunters Hill",2110,"NS",-33.834,151.150,2990000,838000,1150,630,66,74,74,72,55,74,70,76,2.1,56,30,[],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(280,"Putney",2112,"NS",-33.826,151.106,2530000,799000,1040,610,67,75,74,73,57,75,71,77,2.2,58,30,[],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(281,"Denistone",2114,"NS",-33.800,151.090,2300000,780000,980,600,69,77,83,75,59,77,73,79,2.4,62,30,["T"],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(282,"Eastwood",2122,"NS",-33.790,151.080,2415000,790000,1010,600,67,75,83,73,57,75,71,77,2.2,58,30,["T"],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(283,"Marsfield",2122,"NS",-33.777,151.106,2185000,770000,950,600,69,76,74,74,59,76,73,78,2.3,61,30,[],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(284,"North Epping",2121,"NS",-33.762,151.088,2300000,780000,980,600,67,76,74,74,57,76,71,78,2.2,59,30,[],"Ryde district — geographic centre of Sydney with schools, jobs and metro on the doorstep."), S(285,"Castle Hill",2154,"NW",-33.732,151.005,2100000,790000,930,600,72,78,80,69,50,74,70,74,1.8,55,30,["M1"],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(286,"Baulkham Hills",2153,"NW",-33.758,150.990,2000000,780000,900,600,73,79,68,69,51,75,71,75,1.8,56,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(287,"Bella Vista",2153,"NW",-33.740,150.955,2400000,819000,1000,620,76,82,80,71,54,78,74,78,2.0,61,30,["M1"],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(288,"Norwest",2153,"NW",-33.733,150.970,2000000,780000,900,600,73,79,80,69,51,75,71,75,1.8,56,30,["M1"],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(289,"Kellyville",2155,"NW",-33.712,150.955,2100000,790000,930,600,74,80,80,70,52,76,72,76,1.9,58,30,["M1"],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(290,"North Kellyville",2155,"NW",-33.700,150.940,2100000,790000,930,600,74,80,68,70,52,76,72,76,1.9,58,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(291,"Kellyville Ridge",2155,"NW",-33.700,150.920,2000000,780000,900,600,76,82,68,72,55,78,74,78,2.1,62,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(292,"Glenwood",2768,"NW",-33.735,150.930,2100000,790000,930,600,72,78,68,69,50,74,70,74,1.8,55,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(293,"Stanhope Gardens",2768,"NW",-33.720,150.930,2000000,780000,900,600,76,82,68,71,54,78,74,78,2.0,61,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(294,"The Ponds",2769,"NW",-33.705,150.905,2100000,790000,930,600,76,82,68,71,54,78,74,78,2.0,61,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(295,"Cherrybrook",2126,"NW",-33.720,151.045,2100000,790000,930,600,73,79,80,69,51,75,71,75,1.8,56,30,["M1"],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(296,"West Pennant Hills",2125,"NW",-33.745,151.040,2300000,809000,980,610,75,80,68,70,53,76,73,76,1.9,59,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(297,"North Rocks",2151,"NW",-33.772,151.020,2100000,790000,930,600,76,82,68,71,54,78,74,78,2.0,61,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(298,"Glenhaven",2156,"NW",-33.705,151.005,2300000,809000,980,610,72,78,68,68,49,74,70,74,1.7,54,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(299,"Dural",2158,"NW",-33.685,151.020,2300000,809000,980,610,75,81,68,71,53,77,73,77,2.0,60,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(300,"Riverstone",2765,"WS",-33.680,150.860,978000,539000,560,460,82,68,75,75,48,50,74,66,2.0,53,55,["T"],"Blacktown belt — deep affordability with jobs and transport upgrades; growth is concentrating here."), S(301,"Box Hill",2765,"NW",-33.652,150.900,1700000,751000,820,590,76,82,68,71,54,78,74,78,2.0,61,30,[],"Hills metro belt — Metro Northwest repriced it once; town-centre densification is the second act."), S(302,"Richmond",2753,"HW",-33.600,150.750,902000,514000,540,440,74,50,55,58,44,48,58,60,1.9,40,78,["T"],"Hawkesbury — semi-rural value with real flood exposure; buy elevation, not river frontage."), S(303,"Windsor",2756,"HW",-33.610,150.810,902000,514000,540,440,76,52,55,59,46,50,60,62,2.0,43,81,["T"],"Hawkesbury — semi-rural value with real flood exposure; buy elevation, not river frontage."), S(304,"North Richmond",2754,"HW",-33.580,150.720,902000,514000,540,440,75,50,46,58,45,48,59,60,1.9,41,85,[],"Hawkesbury — semi-rural value with real flood exposure; buy elevation, not river frontage."), S(305,"Bligh Park",2756,"HW",-33.635,150.800,855000,507000,530,430,75,51,46,59,45,49,59,61,2.0,42,80,[],"Hawkesbury — semi-rural value with real flood exposure; buy elevation, not river frontage."), S(306,"Manly",2095,"NB",-33.797,151.285,3680000,1089000,1410,780,61,52,50,54,63,68,51,88,1.6,35,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(307,"Fairlight",2094,"NB",-33.795,151.275,3360000,1063000,1340,770,62,52,50,54,64,68,52,88,1.6,36,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(308,"Balgowlah",2093,"NB",-33.792,151.257,3360000,1063000,1340,770,61,51,50,53,63,67,51,87,1.5,34,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(309,"Balgowlah Heights",2093,"NB",-33.803,151.253,3840000,1102000,1450,780,63,52,50,54,65,68,53,88,1.7,37,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(310,"Seaforth",2092,"NB",-33.788,151.240,3840000,1102000,1450,780,63,53,50,55,65,69,53,89,1.7,38,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(311,"Manly Vale",2093,"NB",-33.783,151.262,3040000,1037000,1260,750,64,54,50,55,66,70,54,90,1.8,39,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(312,"Freshwater",2096,"NB",-33.777,151.286,3520000,1076000,1380,770,61,52,50,54,63,68,51,88,1.6,35,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(313,"Curl Curl",2096,"NB",-33.768,151.290,3680000,1089000,1410,780,60,50,50,53,62,66,50,86,1.5,33,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(314,"Dee Why",2099,"NB",-33.752,151.286,2880000,1024000,1220,750,62,52,50,54,64,68,52,88,1.6,36,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(315,"Brookvale",2100,"NB",-33.762,151.270,2720000,1011000,1180,740,60,50,50,52,61,66,50,86,1.4,32,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(316,"Beacon Hill",2100,"NB",-33.752,151.258,3040000,1037000,1260,750,61,51,50,53,63,67,51,87,1.5,34,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(317,"Narraweena",2099,"NB",-33.755,151.270,2944000,1029000,1240,750,63,52,50,54,65,68,53,88,1.7,37,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(318,"Cromer",2099,"NB",-33.735,151.265,3040000,1037000,1260,750,63,52,50,54,65,68,53,88,1.7,37,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(319,"Frenchs Forest",2086,"NB",-33.750,151.240,3200000,1050000,1300,760,63,52,50,54,65,68,53,88,1.7,37,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(320,"Forestville",2087,"NB",-33.765,151.212,3200000,1050000,1300,760,62,52,50,54,64,68,52,88,1.6,36,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(321,"Belrose",2085,"NB",-33.738,151.212,3200000,1050000,1300,760,62,52,50,54,64,68,52,88,1.6,36,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(322,"Terrey Hills",2084,"NB",-33.688,151.226,3360000,1063000,1340,770,60,50,50,52,61,66,50,86,1.4,32,30,[],"Southern Northern Beaches — B-Line buses not rail, but lifestyle scarcity keeps a floor under land."), S(323,"Collaroy",2097,"NB",-33.732,151.301,2970000,974000,1220,710,61,46,38,46,57,62,47,74,1.8,31,55,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(324,"Narrabeen",2101,"NB",-33.713,151.297,2700000,950000,1150,700,58,44,38,45,54,60,44,72,1.5,30,60,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(325,"Elanora Heights",2101,"NB",-33.700,151.272,2700000,950000,1150,700,60,46,38,46,56,62,46,74,1.7,30,34,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(326,"Warriewood",2102,"NB",-33.690,151.290,2700000,950000,1150,700,58,44,38,44,53,60,44,72,1.5,30,30,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(327,"Mona Vale",2103,"NB",-33.677,151.305,2835000,962000,1180,710,62,48,38,48,59,64,48,76,1.9,34,38,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(328,"Newport",2106,"NB",-33.655,151.315,2835000,962000,1180,710,59,45,38,45,55,61,45,73,1.6,30,32,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(329,"Avalon Beach",2107,"NB",-33.635,151.328,2970000,974000,1220,710,62,48,38,47,58,64,48,76,1.9,33,37,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(330,"Palm Beach",2108,"NB",-33.600,151.323,4050000,1069000,1470,750,62,48,38,47,58,64,48,76,1.9,33,37,[],"Northern peninsula — pure lifestyle market; low transit, low supply, long-hold territory."), S(331,"Bexley North",2207,"SS",-33.938,151.115,1710000,652000,820,550,74,68,81,77,60,66,76,78,2.4,61,37,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(332,"Bardwell Park",2207,"SS",-33.930,151.125,1764000,657000,840,550,70,64,81,75,56,62,72,74,2.1,55,31,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(333,"Banksia",2216,"SS",-33.945,151.140,1530000,635000,770,540,74,68,81,77,60,66,76,78,2.4,61,37,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(334,"Brighton-Le-Sands",2216,"SS",-33.960,151.155,1710000,652000,820,550,73,66,72,76,59,64,75,76,2.2,59,55,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(335,"Monterey",2217,"SS",-33.972,151.152,1800000,660000,850,550,70,64,72,74,55,62,72,74,2.0,54,50,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(336,"Ramsgate",2217,"SS",-33.982,151.145,1800000,660000,850,550,72,66,72,76,58,64,74,76,2.2,58,34,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(337,"Sans Souci",2219,"SS",-33.992,151.132,1890000,668000,880,550,73,66,72,76,59,64,75,76,2.2,59,35,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(338,"Carlton",2218,"SS",-33.970,151.120,1620000,644000,800,540,74,68,81,78,61,66,76,78,2.4,62,38,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(339,"Allawah",2218,"SS",-33.970,151.112,1584000,640000,790,540,71,65,81,75,57,63,73,75,2.1,56,32,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(340,"Penshurst",2222,"SS",-33.965,151.088,1710000,652000,820,550,73,67,81,77,59,65,75,77,2.3,60,36,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(341,"Mortdale",2223,"SS",-33.968,151.078,1656000,647000,810,540,71,65,81,75,57,63,73,75,2.1,56,32,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(342,"Oatley",2223,"SS",-33.980,151.078,1980000,676000,900,560,73,67,81,77,59,65,75,77,2.3,60,36,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(343,"Peakhurst",2210,"SS",-33.962,151.058,1710000,652000,820,550,73,66,72,76,59,64,75,76,2.2,59,35,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(344,"Lugarno",2210,"SS",-33.980,151.042,1980000,676000,900,560,72,66,72,76,58,64,74,76,2.2,58,34,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(345,"Riverwood",2210,"SS",-33.950,151.050,1530000,635000,770,540,74,68,81,77,60,66,76,78,2.4,61,37,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(346,"Beverly Hills",2209,"SS",-33.949,151.080,1710000,652000,820,550,73,66,81,76,59,64,75,76,2.2,59,35,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(347,"Narwee",2209,"SS",-33.945,151.068,1620000,644000,800,540,73,66,81,76,59,64,75,76,2.2,59,35,["T"],"St George — airport-side middle ring with rail depth and steady rezoning."), S(348,"Blakehurst",2221,"SS",-33.988,151.108,1980000,676000,900,560,70,64,72,74,55,62,72,74,2.0,54,30,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(349,"South Hurstville",2221,"SS",-33.978,151.105,1710000,652000,820,550,72,66,72,76,58,64,74,76,2.2,58,34,[],"St George — airport-side middle ring with rail depth and steady rezoning."), S(350,"Sylvania",2224,"SS",-34.010,151.100,1750000,780000,880,620,71,57,64,67,57,67,61,77,1.7,44,30,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(351,"Sylvania Waters",2224,"SS",-34.020,151.108,2188000,829000,1010,640,70,56,64,67,56,66,60,76,1.6,43,50,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(352,"Jannali",2226,"SS",-34.015,151.062,1662000,770000,850,620,71,58,73,68,57,68,61,78,1.8,45,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(353,"Como",2226,"SS",-34.005,151.065,1838000,790000,910,620,71,57,73,67,57,67,61,77,1.7,44,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(354,"Oyster Bay",2225,"SS",-34.000,151.078,1838000,790000,910,620,73,59,64,69,59,69,63,79,1.9,48,30,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(355,"Gymea",2227,"SS",-34.032,151.085,1750000,780000,880,620,70,56,73,67,56,66,60,76,1.6,43,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(356,"Gymea Bay",2227,"SS",-34.045,151.088,1925000,799000,930,630,71,58,64,68,57,68,61,78,1.8,45,30,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(357,"Kirrawee",2232,"SS",-34.033,151.070,1750000,780000,880,620,72,58,73,68,58,68,62,78,1.8,46,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(358,"Engadine",2233,"SS",-34.065,151.012,1575000,760000,830,610,70,56,73,66,55,66,60,76,1.6,42,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(359,"Heathcote",2233,"SS",-34.087,151.008,1488000,751000,800,610,73,59,73,69,59,69,63,79,1.9,48,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(360,"Bangor",2234,"SS",-34.020,151.032,1662000,770000,850,620,73,58,64,68,59,68,63,78,1.9,47,30,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(361,"Menai",2234,"SS",-34.015,151.010,1662000,770000,850,620,73,58,64,68,59,68,63,78,1.9,47,30,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(362,"Woolooware",2230,"SS",-34.045,151.140,1838000,790000,910,620,71,57,73,67,57,67,61,77,1.7,44,30,["T"],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(363,"Caringbah South",2229,"SS",-34.058,151.118,2012000,809000,960,630,70,56,64,66,55,66,60,76,1.6,42,30,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(364,"Kurnell",2231,"SS",-34.008,151.204,1488000,751000,800,610,71,58,64,68,57,68,61,78,1.8,45,60,[],"Sutherland Shire — the Sutherland–Menai SA3 posted +9.2% last year; family-belt demand is resilient."), S(365,"Rosebery",2018,"SS",-33.918,151.205,1850000,800000,920,660,67,71,74,77,59,49,79,75,2.7,76,38,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(366,"Eastlakes",2018,"SS",-33.930,151.212,1572000,770000,830,650,68,72,74,78,60,50,80,76,2.8,78,40,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(367,"Pagewood",2035,"SS",-33.940,151.222,1758000,790000,890,660,69,73,74,79,61,51,81,77,2.9,80,42,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(368,"Hillsdale",2036,"SS",-33.950,151.228,1480000,760000,800,640,67,71,74,77,59,49,79,75,2.7,76,38,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype."), S(369,"Botany",2019,"SS",-33.945,151.196,1758000,790000,890,660,66,70,74,77,58,48,78,74,2.6,75,50,[],"Inner-south Bayside — CBD-close convenience with a heavy apartment pipeline; buy on position, not hype.") ]; /* ============================================================ TRANSPORT / ROADS / POI (unchanged geometry from v1) ============================================================ */ const WP={Epping:[-33.772,151.082],MacquariePark:[-33.777,151.126],Chatswood:[-33.797,151.180],NorthSyd:[-33.840,151.207], MartinPl:[-33.867,151.210],Central:[-33.883,151.206],HunterSt:[-33.865,151.208],Barangaroo:[-33.860,151.203], Penrith:[-33.751,150.694],Blacktown:[-33.771,150.907],StMarys:[-33.762,150.774],Strathfield:[-33.873,151.095], Airport:[-33.884,150.740],AirportBP:[-33.878,150.735],Bradfield:[-33.892,150.752],Luddenham:[-33.870,150.730], OrchardHills:[-33.795,150.755],Rouse:[-33.687,150.916],Tallawong:[-33.693,150.906]}; const sc=(id)=>{const s=SUBURBS.find(x=>x.id===id);return [s.lat,s.lng];}; const LINES=[ {id:"m1_open",kind:"open",name:"M1 Metro (open)",pts:[WP.Rouse,WP.Tallawong,WP.Epping,WP.MacquariePark,WP.Chatswood,WP.NorthSyd,WP.Barangaroo,WP.MartinPl,WP.Central,sc(13)]}, {id:"m1_bk",kind:"future",year:"2026",name:"M1 Southwest",pts:[sc(13),sc(1),sc(2),sc(3),sc(4),sc(5),sc(6),sc(7),sc(8),sc(9),sc(10)]}, {id:"mw",kind:"future",year:"2032",name:"Metro West",pts:[sc(22),sc(21),sc(20),sc(19),sc(18),sc(17),sc(24),sc(23),WP.HunterSt]}, {id:"wsa",kind:"future",year:"2027",name:"WSA line",pts:[WP.StMarys,WP.OrchardHills,WP.Luddenham,WP.AirportBP,WP.Airport,WP.Bradfield]}, {id:"t1",kind:"train",name:"T1 Western",pts:[WP.Penrith,WP.StMarys,WP.Blacktown,sc(21),WP.Strathfield,WP.Central]} ]; const ROADS=[ {id:"m4",pts:[[-33.751,150.694],[-33.79,150.86],[-33.82,150.99],[-33.86,151.08],[-33.884,151.14],[-33.887,151.18]]}, {id:"m8",pts:[[-33.99,150.86],[-33.96,151.00],[-33.93,151.10],[-33.915,151.165],[-33.90,151.20]]}, {id:"m7",pts:[[-33.70,150.905],[-33.80,150.86],[-33.90,150.855],[-33.99,150.855]]}, {id:"m2",pts:[[-33.72,150.985],[-33.755,151.06],[-33.80,151.13],[-33.815,151.175]]}, {id:"m1mwy",pts:[[-33.66,151.145],[-33.70,151.11],[-33.76,151.14],[-33.82,151.20]]} ]; const AMENITIES=[ {t:"H",lat:-33.800,lng:150.990,n:"Westmead Hospital"},{t:"H",lat:-33.889,lng:151.181,n:"RPA Hospital"}, {t:"H",lat:-33.966,lng:151.134,n:"St George Hospital"},{t:"H",lat:-33.820,lng:151.190,n:"Royal North Shore"}, {t:"H",lat:-33.920,lng:150.920,n:"Liverpool Hospital"},{t:"H",lat:-33.918,lng:151.033,n:"Bankstown Hospital"}, {t:"H",lat:-33.750,lng:150.710,n:"Nepean Hospital"}, {t:"R",lat:-33.815,lng:151.005,n:"Westfield Parramatta"},{t:"R",lat:-33.891,lng:151.250,n:"Westfield Bondi Jct"}, {t:"R",lat:-33.797,lng:151.183,n:"Westfield Chatswood"},{t:"R",lat:-33.877,lng:151.104,n:"Westfield Burwood"}, {t:"R",lat:-33.967,lng:151.102,n:"Westfield Hurstville"},{t:"R",lat:-33.777,lng:151.127,n:"Macquarie Centre"}, {t:"E",lat:-33.888,lng:151.187,n:"University of Sydney"},{t:"E",lat:-33.917,lng:151.231,n:"UNSW"}, {t:"E",lat:-33.775,lng:151.113,n:"Macquarie University"},{t:"E",lat:-33.810,lng:151.030,n:"Western Sydney Uni"}, {t:"R",lat:-34.036,lng:151.104,n:"Westfield Miranda"},{t:"R",lat:-33.769,lng:150.907,n:"Westpoint Blacktown"}, {t:"R",lat:-33.838,lng:150.992,n:"Stockland Merrylands"},{t:"R",lat:-33.767,lng:150.818,n:"Westfield Mt Druitt"}, {t:"H",lat:-34.062,lng:150.822,n:"Campbelltown Hospital"},{t:"H",lat:-33.870,lng:150.950,n:"Fairfield Hospital"}, {t:"E",lat:-34.070,lng:150.800,n:"WSU Campbelltown"} ]; const SCHOOLS=[ {lat:-33.790,lng:151.050,n:"James Ruse Agricultural"},{lat:-33.900,lng:151.223,n:"Sydney Boys/Girls High"}, {lat:-33.833,lng:151.190,n:"North Sydney Boys"},{lat:-33.894,lng:151.150,n:"Fort Street High"}, {lat:-33.870,lng:151.090,n:"Homebush Boys"},{lat:-33.760,lng:150.990,n:"Baulkham Hills High"}, {lat:-33.797,lng:151.176,n:"Chatswood High"},{lat:-33.710,lng:151.100,n:"Normanhurst / Hornsby"} ]; /* ============================================================ STATE / FACTORS / LAYERS ============================================================ */ const FACTORS=[ {k:"growth",label:"Growth momentum",color:"#eab34c"}, {k:"future",label:"Future transit",color:"#6ea6ff"}, {k:"transport",label:"Transport & commute",color:"#37b6a4"}, {k:"gentr",label:"Gentrification",color:"#9b8cf0"}, {k:"schools",label:"Schools",color:"#5fbf8a"}, {k:"zoning",label:"Zoning upside",color:"#d98a5b"}, {k:"amenity",label:"Everyday amenity",color:"#e0a3d0"}, {k:"yield",label:"Rental yield",color:"#66c7d6"} ]; const PRESETS={ balanced:{growth:18,future:16,transport:12,gentr:14,schools:10,zoning:10,amenity:12,yield:8}, growth:{growth:24,future:20,transport:8,gentr:18,schools:8,zoning:12,amenity:6,yield:4}, yield:{growth:10,future:10,transport:16,gentr:6,schools:6,zoning:8,amenity:16,yield:28} }; const LAYER_DEFS=[ {k:"score",label:"Score markers",kind:"gl",color:"#eab34c",on:true}, {k:"heat",label:"Score heatmap",kind:"gl",color:"#eab34c",on:false}, {k:"labels",label:"Suburb labels",kind:"gl",color:"#c3d0dd",on:true}, {k:"future",label:"Future transit lines",kind:"dash",color:"#6ea6ff",on:true}, {k:"open",label:"Metro & rail (open)",kind:"solid",color:"#37b6a4",on:true}, {k:"roads",label:"Motorways",kind:"solid",color:"#d98a5b",on:false}, {k:"amen",label:"Hospitals · retail · uni",kind:"gl",color:"#e0a3d0",on:false}, {k:"schools",label:"Top schools",kind:"gl",color:"#5fbf8a",on:false}, {k:"water",label:"Water & coastline",kind:"gl",color:"#2b6f8f",on:true} ]; const state={ type:"unit",bed:2,bath:1,car:1,land:1,cond:1, strategy:"balanced",weights:{...PRESETS.balanced}, layers:Object.fromEntries(LAYER_DEFS.map(l=>[l.k,l.on])), budget:null,budgetDim:false,sort:"score",selected:null,pinned:[],asking:null, workplace:"cbd",io:false,strataOv:null,notes:{}, flt:{region:"all",q:"",minY:null,maxCm:null},shortlist:[], fin:{dep:null,inc:null,inc2:null,exp:null,debt:0,kids:0,who:"oo",fhb:false,rent:null,buyPower:null,detail:null} }; /* ============================================================ PERSISTENCE — profile, brief, weights, pins & notes survive reloads ============================================================ */ const LSKEY="meridian_v3"; let saveT=null; function save(){ clearTimeout(saveT); saveT=setTimeout(()=>{try{ const f=state.fin; localStorage.setItem(LSKEY,JSON.stringify({ type:state.type,bed:state.bed,bath:state.bath,car:state.car,land:state.land,cond:state.cond, strategy:state.strategy,weights:state.weights,layers:state.layers, budget:state.budget,budgetDim:state.budgetDim,sort:state.sort,pinned:state.pinned, workplace:state.workplace,io:state.io,strataOv:state.strataOv,notes:state.notes, flt:{region:state.flt.region,minY:state.flt.minY,maxCm:state.flt.maxCm},shortlist:state.shortlist, fin:{dep:f.dep,inc:f.inc,inc2:f.inc2,exp:f.exp,debt:f.debt,kids:f.kids,who:f.who,fhb:f.fhb,rent:f.rent} })); }catch(e){}},250); } function load(){ try{ const d=JSON.parse(localStorage.getItem(LSKEY)||"null");if(!d)return; ["type","bed","bath","car","land","cond","strategy","budget","budgetDim","sort","workplace","io","strataOv"].forEach(k=>{if(d[k]!==undefined&&d[k]!==null)state[k]=d[k];}); if(d.weights)state.weights={...state.weights,...d.weights}; if(d.layers)state.layers={...state.layers,...d.layers}; if(Array.isArray(d.pinned))state.pinned=d.pinned.filter(id=>SUBURBS.some(s=>s.id===id)).slice(0,3); if(d.notes)state.notes=d.notes; if(d.flt){state.flt.region=d.flt.region||'all';state.flt.minY=d.flt.minY??null;state.flt.maxCm=d.flt.maxCm??null;} if(Array.isArray(d.shortlist))state.shortlist=d.shortlist.filter(e=>SUBURBS.some(x=>x.id===e.sid)); if(d.fin)Object.assign(state.fin,d.fin); }catch(e){} } /* ============================================================ TAX / DUTY / LMI / SERVICEABILITY — real 2025–26 rules ============================================================ */ function netAnnual(gross){ // 2025-26 resident brackets + 2% Medicare if(!gross)return 0; const br=[[18200,0],[45000,.16],[135000,.30],[190000,.37],[Infinity,.45]]; let t=0,prev=0; for(const[u,r] of br){const amt=Math.min(gross,u)-prev;if(amt>0)t+=amt*r;prev=u;if(gross<=u)break;} if(gross>26000)t+=gross*.02; return gross-t; } function stampNSW(Pr){ // Revenue NSW 2025-26 (verified $800k→$30,412; $1.2M→$48,412) const br=[[18000,0,0,.0125],[36000,18000,225,.015],[97000,36000,495,.0175], [372000,97000,1562,.035],[1240000,372000,11152,.045],[3721000,1240000,50212,.055],[Infinity,3721000,186667,.07]]; for(const[u,l,base,r] of br){ if(Pr<=u) return Math.max(20, base+(Pr-l)*r); } } function dutyFor(Pr,fhbOwner){ const full=stampNSW(Pr); if(!fhbOwner)return full; if(Pr<=MKT.fhbExempt)return 0; if(Pr<MKT.fhbTaper)return full*(Pr-MKT.fhbExempt)/(MKT.fhbTaper-MKT.fhbExempt); // verified vs Revenue NSW $950k example return full; } function lmiRate(lvr){ if(lvr<=.80)return 0; if(lvr<=.85)return .011; if(lvr<=.90)return .020; return .038; } function hemFloor(){ const couple=!!state.fin.inc2; return (couple?4300:2750)+state.fin.kids*600; } function repay(loan,annualPct,years=30){ const r=annualPct/1200,n=years*12; return loan*r/(1-Math.pow(1+r,-n)); } function maxLoanFromIncome(){ const f=state.fin; const net=(netAnnual(f.inc||0)+netAnnual(f.inc2||0))/12; if(net<=0)return 0; const rentCr=f.who==="inv"&&f.rent? f.rent*52/12*0.8 : (f.who==="oo"&&f.rent? f.rent*52/12*0.8:0); const exp=Math.max(f.exp||0,hemFloor()); const surplus=net+rentCr-exp-(f.debt||0); if(surplus<=0)return 0; const assess=(f.who==="inv"?MKT.varINV:MKT.varOO)+MKT.buffer; const r=assess/1200,n=360; return surplus*(1-Math.pow(1+r,-n))/r; } function fhgOK(Pr){ const f=state.fin; return f.fhb&&f.who==="oo"&&Pr<=MKT.fhgCap&&(f.dep||0)>=0.05*Pr; } function solveBuyPower(){ const f=state.fin; if(!f.dep||f.dep<10000||(!f.inc&&!f.inc2)){f.buyPower=null;f.detail=null;return;} const maxLoan=maxLoanFromIncome(); const feasible=(Pr)=>{ const duty=dutyFor(Pr,f.fhb&&f.who==="oo"); let need=Pr+duty+MKT.buyCosts-f.dep; if(need<0)need=0; const fhg=fhgOK(Pr); let lvr=need/Pr; const cap=fhg?0.95:(f.who==="inv"?0.90:0.95); let lmi=fhg?0:need*lmiRate(lvr); const loan=need+lmi; lvr=loan/Pr; return {ok:loan<=maxLoan&&lvr<=cap+1e-9,loan,duty,lmi,lvr,fhg}; }; let lo=50000,hi=f.dep+maxLoan+50000,best=null; if(!feasible(lo).ok){f.buyPower=0;f.detail=null;return;} for(let i=0;i<44;i++){const mid=(lo+hi)/2;const r=feasible(mid);if(r.ok){lo=mid;best=r;best.P=mid;}else hi=mid;} f.buyPower=Math.floor(lo/1000)*1000; const r=feasible(f.buyPower); f.detail={...r,P:f.buyPower,maxLoan,repayM:repay(r.loan,f.who==="inv"?MKT.varINV:MKT.varOO)}; } /* ============================================================ COMMUTE MODEL — anchor-aware minutes estimate ============================================================ */ const ANCHORS={ cbd:{n:"Sydney CBD",lat:-33.868,lng:151.209}, parra:{n:"Parramatta",lat:-33.817,lng:151.005}, macq:{n:"Macquarie Park",lat:-33.777,lng:151.126}, liv:{n:"Liverpool",lat:-33.920,lng:150.923}, apt:{n:"Airport / Mascot",lat:-33.936,lng:151.166} }; function commuteMin(s){ const a=ANCHORS[state.workplace]||ANCHORS.cbd; const d=hav(s.lat,s.lng,a.lat,a.lng); const base=s.tn>=78?38:s.tn>=58?32:24; // km/h by rail quality const ov=s.tn>=78?8:s.tn>=58?11:14; // access/wait overhead const eff=base*(1+Math.min(d,50)/55); // express effect on longer runs return Math.max(6,Math.round(d/eff*60+ov+d*0.15)); } function cmScoreDyn(s){const m=commuteMin(s);return clamp(105-m*1.35,15,100);} function marginalRate(g){if(!g)return 0;if(g>190000)return .47;if(g>135000)return .39;if(g>45000)return .32;if(g>18200)return .18;return 0;} function cycleStage(s){ if(s.g>=80&&s.ge<62)return["Early cycle","#5fbf8a"]; if(s.g>=74)return["Rising","#eab34c"]; if(s.ge>=76&&s.g<72)return["Mature","#9b8cf0"]; if(s.g<64)return["Cooling","#7d92a8"]; return["Steady","#37b6a4"]; } function trend12(s){return +((s.g-56)*0.32).toFixed(1);} // modelled past-12-mo %, calibrated to Cotality SA3 leaders /* ============================================================ SCORING ENGINE (as v1) ============================================================ */ function clamp(v,a,b){return Math.max(a,Math.min(b,v));} function yieldPct(s){ let price,rent; if(state.type==="house"){price=s.h;rent=s.rh;} else if(state.type==="townhouse"){price=s.h?Math.round(s.h*0.72):s.u;rent=Math.round(s.rh*0.55+s.ru*0.6);} else{price=s.u;rent=s.ru;} if(!price||!rent)return null; return +(rent*52/price*100).toFixed(1); } function yieldScore(s){const y=yieldPct(s);if(y===null)return 40;return clamp((y-2.5)/(5.4-2.5)*100,0,100);} function transportScore(s){return s.tn*0.55+cmScoreDyn(s)*0.45;} function factorRaw(s,k){switch(k){ case"growth":return s.g;case"future":return s.ft;case"transport":return transportScore(s); case"gentr":return s.ge;case"schools":return s.sc;case"zoning":return s.zo; case"amenity":return s.am;case"yield":return yieldScore(s);}} function riskNorm(s){return (s.sup*0.6+s.fld*0.4)/100;} function composite(s){ let wsum=0,acc=0; for(const f of FACTORS){const w=state.weights[f.k]||0;wsum+=w;acc+=w*factorRaw(s,f.k);} const raw=wsum?acc/wsum:0; return {raw,adj:raw*(1-0.16*riskNorm(s))}; } function medianFor(s){ if(state.type==="house")return s.h; if(state.type==="townhouse")return s.h?Math.round(s.h*0.72):s.u; return s.u; } function hasComp(s){return medianFor(s)>0;} let RANKED=[]; function computeAll(){ const rows=SUBURBS.map(s=>{ const c=composite(s); return {s,score:c.adj,y:yieldPct(s),median:medianFor(s),risk:riskNorm(s),fc:fc1(s),fc3:fc3(s)}; }); rows.sort((a,b)=>b.score-a.score); rows.forEach((r,i)=>r.rank=i+1); return rows; } /* ============================================================ FORECAST ENGINE — anchored to Big-4 spread, scenario refresh ============================================================ */ const FC={scenario:null}; function mulberry(a){return function(){a|=0;a=a+0x6D2B79F5|0;let t=Math.imul(a^a>>>15,1|a);t=t+Math.imul(t^t>>>7,61|t)^t;return((t^t>>>14)>>>0)/4294967296;}} function rollScenario(silent){ const r=Math.random; const b=MKT.banks[Math.floor(r()*MKT.banks.length)]; const jit=(r()*2-1)*0.8; const paths=[["RBA holds 4.35% through 2026",0,.45],["cuts resume mid-2027",1.3,.35],["one further hike lands",-1.6,.20]]; let x=r(),path=paths[0]; for(const p of paths){if(x<p[2]){path=p;break;}x-=p[2];} FC.scenario={anchor:b[0],base:b[1]+jit,pathLabel:path[0],pathAdj:path[1],seed:(r()*1e9)|0}; if(!silent){RANKED=computeAll();renderRank();renderMarkers();renderPulse();if(state.selected)refreshDetail();} } function fc1(s){ // 12-mo % for current dwelling type if(!FC.scenario)rollScenario(true); const sc=FC.scenario; const unitTilt=state.type==="house"?-0.9:0.9; // units outperforming in the softening phase (Cotality) const tilt=(s.g-70)*.06+(s.ft-70)*.045+(s.ge-64)*.025+(s.zo-64)*.02-riskNorm(s)*2.6; const noise=(mulberry(sc.seed+s.id*7919)()*2-1)*0.7; return clamp(sc.base+sc.pathAdj+unitTilt+tilt+noise,-6.5,9.5); } function fc3(s){ // cumulative 3-yr %: current year + rate-normalisation recovery const y1=fc1(s); const rec=4.8+(s.ft-70)*.05+(s.g-70)*.05; return clamp(y1+rec+y1*0.4,-4,27); } function fcSpread(s){return 1.8+riskNorm(s)*2.4;} /* ============================================================ 10-YEAR EQUITY PROJECTION ============================================================ */ function equityPath(s,r,price,loan){ const pts=[];let v=price,bal=loan; const rate=(state.fin.who==="inv"?MKT.varINV:MKT.varOO)/100; const mrep=repay(loan||1,rate*100); const lr=clamp(4.2+(s.g-70)*.05+(s.ft-70)*.03,2,7.5); // long-run growth from momentum + transit pipeline for(let y=0;y<=10;y++){ pts.push({y,v,bal:Math.max(0,bal),eq:v-Math.max(0,bal)}); const g=y===0?r.fc:y<3?(r.fc3-r.fc)/2:lr; v*=1+g/100; for(let m=0;m<12;m++){bal=Math.max(0,bal+bal*rate/12-mrep);} } return pts; } function eqChart(s,r,price,loan){ const pts=equityPath(s,r,price,loan); const Wc=330,Hc=132,pad={l:8,r:52,t:12,b:18}; const maxV=Math.max(...pts.map(p=>p.v))*1.04; const X=y=>pad.l+(Wc-pad.l-pad.r)*y/10; const Y=v=>Hc-pad.b-(Hc-pad.t-pad.b)*v/maxV; const line=key=>pts.map((p,i)=>(i?'L':'M')+X(p.y).toFixed(1)+' '+Y(p[key]).toFixed(1)).join(' '); const eqArea=line('eq')+` L ${X(10).toFixed(1)} ${Y(0)} L ${X(0)} ${Y(0)} Z`; const p5=pts[5],p10=pts[10]; let ticks='';[0,5,10].forEach(y=>{ticks+=`<text x="${X(y)}" y="${Hc-4}" text-anchor="middle" font-size="8" fill="#5f7387" font-family="IBM Plex Mono">yr ${y}</text>`;}); return `<svg width="100%" viewBox="0 0 ${Wc} ${Hc}" style="display:block"> <line x1="${pad.l}" y1="${Y(0)}" x2="${Wc-pad.r}" y2="${Y(0)}" stroke="#26384a" stroke-width="1"/> <path d="${eqArea}" fill="rgba(95,191,138,.13)"/> <path d="${line('eq')}" fill="none" stroke="#5fbf8a" stroke-width="1.8"/> <path d="${line('v')}" fill="none" stroke="#eab34c" stroke-width="1.6" stroke-dasharray="1 3"/> <circle cx="${X(5)}" cy="${Y(p5.eq)}" r="2.6" fill="#5fbf8a"/> <circle cx="${X(10)}" cy="${Y(p10.eq)}" r="2.6" fill="#5fbf8a"/> <text x="${X(5)+5}" y="${Y(p5.eq)-5}" font-size="8.5" fill="#5fbf8a" font-family="IBM Plex Mono">${fmtMoney(p5.eq)}</text> <text x="${X(10)+5}" y="${Y(p10.eq)+3}" font-size="8.5" fill="#5fbf8a" font-family="IBM Plex Mono">${fmtMoney(p10.eq)}</text> <text x="${X(10)+5}" y="${Y(p10.v)+3}" font-size="8.5" fill="#eab34c" font-family="IBM Plex Mono">${fmtMoney(p10.v)}</text> ${ticks} </svg>`; } /* ============================================================ COLORS / FORMAT ============================================================ */ function lerp(a,b,t){return a+(b-a)*t;} function hex2rgb(h){h=h.replace('#','');return [parseInt(h.slice(0,2),16),parseInt(h.slice(2,4),16),parseInt(h.slice(4,6),16)];} function rgb2hex(r){return '#'+r.map(v=>Math.round(clamp(v,0,255)).toString(16).padStart(2,'0')).join('');} const RAMP=[[35,"#3a4a78"],[55,"#2fa39a"],[72,"#5aa46a"],[82,"#eab34c"],[95,"#f0894e"]]; function scoreColor(v){ v=clamp(v,0,100); for(let i=0;i<RAMP.length-1;i++){const[v0,c0]=RAMP[i],[v1,c1]=RAMP[i+1]; if(v<=v1){const t=(v-v0)/(v1-v0);const a=hex2rgb(c0),b=hex2rgb(c1); return rgb2hex([lerp(a[0],b[0],t),lerp(a[1],b[1],t),lerp(a[2],b[2],t)]);}} return RAMP[RAMP.length-1][1]; } function hexA(hex,a){const[r,g,b]=hex2rgb(hex);return `rgba(${r},${g},${b},${a})`;} function fmtMoney(n){if(!n)return "—";if(n>=1e6)return "$"+(n/1e6).toFixed(n%1e6===0?1:2)+"M";return "$"+Math.round(n/1000)+"k";} function fmtFull(n){return "$"+Math.round(n).toLocaleString('en-AU');} function parseNum(v){const n=parseFloat(String(v).replace(/[^0-9.]/g,''));return isFinite(n)&&n>0?n:null;} function pct(n,dp=1){return (n>0?'+':'')+n.toFixed(dp)+'%';} /* ============================================================ LISTINGS DEEP-LINKS (real portals, pre-filtered) ============================================================ */ function listingCap(){ return state.fin.buyPower||state.budget||null; } function slug(s){return s.name.toLowerCase().replace(/\s+/g,'-');} function reaURL(s,bedOv){ const type=state.type==="unit"?"unit+apartment":state.type==="townhouse"?"townhouse":"house"; const cap=listingCap(); const price=cap?`-between-0-${Math.round(cap)}`:""; const beds=`-with-${bedOv||state.bed}-bedrooms`; return `https://www.realestate.com.au/buy/property-${type}${beds}${price}-in-${slug(s)},+nsw+${s.pc}/list-1`; } function domainURL(s){ const type=state.type==="unit"?"apartment":state.type==="townhouse"?"town-house":"house"; const cap=listingCap(); const price=cap?`&price=0-${Math.round(cap)}`:""; return `https://www.domain.com.au/sale/?suburb=${slug(s)}-nsw-${s.pc}&ptype=${type}&bedrooms=${state.bed}${price}`; } /* ============================================================ MAP (geometry engine unchanged from v1) ============================================================ */ const SVGNS="http://www.w3.org/2000/svg"; const map=document.getElementById('map'); function el(tag,attrs){const e=document.createElementNS(SVGNS,tag);for(const k in attrs)e.setAttribute(k,attrs[k]);return e;} function polyPts(pts){return pts.map(([la,ln])=>P(la,ln).join(',')).join(' ');} function buildBase(){ map.innerHTML=''; const defs=el('defs',{}); defs.innerHTML=` <radialGradient id="landg" cx="42%" cy="38%" r="80%"><stop offset="0%" stop-color="#131f2b"/><stop offset="60%" stop-color="#0e1620"/><stop offset="100%" stop-color="#090e15"/></radialGradient> <linearGradient id="waterg" x1="0" y1="0" x2="1" y2="1"><stop offset="0%" stop-color="#0f2536"/><stop offset="100%" stop-color="#0a1a27"/></linearGradient> <filter id="glowF" x="-40%" y="-40%" width="180%" height="180%"><feGaussianBlur stdDeviation="3.2" result="b"/><feMerge><feMergeNode in="b"/><feMergeNode in="SourceGraphic"/></feMerge></filter> <radialGradient id="heatg"><stop offset="0%" stop-color="#eab34c" stop-opacity=".55"/><stop offset="100%" stop-color="#eab34c" stop-opacity="0"/></radialGradient>`; map.appendChild(defs); const gView=el('g',{id:'view'});map.appendChild(gView); gView.appendChild(el('rect',{x:0,y:0,width:W,height:H,fill:'url(#landg)'})); const grid=el('g',{id:'grid',opacity:.5}); for(let x=0;x<=W;x+=62.5)grid.appendChild(el('line',{x1:x,y1:0,x2:x,y2:H,stroke:'#1a2836','stroke-width':.6})); for(let y=0;y<=H;y+=63.3)grid.appendChild(el('line',{x1:0,y1:y,x2:W,y2:y,stroke:'#1a2836','stroke-width':.6})); gView.appendChild(grid); const water=el('g',{id:'water'}); const LL=(arr)=>arr.map(([la,ln],i)=>(i?'L ':'M ')+P(la,ln).map(v=>v.toFixed(1)).join(' ')).join(' '); // Tasman Sea — Palm Beach to Royal National Park const coast=[[-33.56,151.345],[-33.598,151.328],[-33.632,151.334],[-33.655,151.322],[-33.676,151.312],[-33.712,151.303],[-33.732,151.307],[-33.752,151.292],[-33.776,151.292],[-33.796,151.291],[-33.818,151.303],[-33.827,151.297],[-33.839,151.284],[-33.849,151.286],[-33.869,151.285],[-33.889,151.279],[-33.914,151.269],[-33.949,151.261],[-33.979,151.251],[-33.999,151.247],[-34.008,151.232],[-34.044,151.172],[-34.074,151.166],[-34.13,151.156]]; water.appendChild(el('path',{d:LL(coast)+` L 1060 ${H+30} L 1060 -30 Z`,fill:'url(#waterg)'})); // Sydney Harbour + Parramatta River water.appendChild(el('path',{d:LL([[-33.842,151.283],[-33.848,151.258],[-33.855,151.238],[-33.852,151.222],[-33.858,151.205],[-33.860,151.188],[-33.856,151.170],[-33.847,151.156]]),fill:'none',stroke:'url(#waterg)','stroke-width':13,'stroke-linecap':'round','stroke-linejoin':'round',opacity:.95})); water.appendChild(el('path',{d:LL([[-33.847,151.156],[-33.840,151.100],[-33.826,151.060],[-33.818,151.020],[-33.810,150.995]]),fill:'none',stroke:'url(#waterg)','stroke-width':7,'stroke-linecap':'round',opacity:.85})); // Middle Harbour water.appendChild(el('path',{d:LL([[-33.827,151.297],[-33.810,151.260],[-33.795,151.240],[-33.790,151.225]]),fill:'none',stroke:'url(#waterg)','stroke-width':8,'stroke-linecap':'round',opacity:.85})); // Pittwater water.appendChild(el('path',{d:LL([[-33.607,151.302],[-33.640,151.294],[-33.672,151.287],[-33.690,151.284]]),fill:'none',stroke:'url(#waterg)','stroke-width':8,'stroke-linecap':'round',opacity:.85})); // Botany Bay water.appendChild(el('path',{d:LL([[-33.963,151.185],[-33.972,151.157],[-33.999,151.152],[-34.008,151.187],[-34.000,151.220],[-33.984,151.230],[-33.967,151.208]])+' Z',fill:'url(#waterg)'})); // Georges River water.appendChild(el('path',{d:LL([[-33.999,151.152],[-33.985,151.112],[-33.973,151.072],[-33.958,151.038],[-33.944,151.004],[-33.938,150.972]]),fill:'none',stroke:'url(#waterg)','stroke-width':6,'stroke-linecap':'round',opacity:.8})); // Port Hacking water.appendChild(el('path',{d:LL([[-34.056,151.158],[-34.068,151.105],[-34.062,151.052]]),fill:'none',stroke:'url(#waterg)','stroke-width':6,'stroke-linecap':'round',opacity:.8})); // Nepean / Hawkesbury water.appendChild(el('path',{d:LL([[-33.570,150.735],[-33.665,150.685],[-33.780,150.658],[-33.900,150.645]]),fill:'none',stroke:'url(#waterg)','stroke-width':5,'stroke-linecap':'round',opacity:.6})); gView.appendChild(water); ['l-heat','l-roads','l-open','l-future','l-amen','l-schools','l-markers'].forEach(id=>gView.appendChild(el('g',{id}))); const cbd=el('g',{id:'cbd'});const[cx,cy]=P(-33.868,151.209); cbd.appendChild(el('circle',{cx,cy,r:6,fill:'none',stroke:'#eab34c','stroke-width':1.4,opacity:.9})); cbd.appendChild(el('circle',{cx,cy,r:2,fill:'#eab34c'})); const cl=el('text',{x:cx+9,y:cy+3,fill:'#eab34c','font-size':10,'font-family':'IBM Plex Mono','letter-spacing':'.1em'});cl.textContent='CBD'; cbd.appendChild(cl);gView.appendChild(cbd); } function renderRoads(){ const g=map.querySelector('#l-roads');g.innerHTML=''; g.style.display=state.layers.roads?'':'none';if(!state.layers.roads)return; ROADS.forEach(r=>{ g.appendChild(el('polyline',{points:polyPts(r.pts),fill:'none',stroke:'#d98a5b','stroke-width':2.4,'stroke-linecap':'round','stroke-linejoin':'round',opacity:.5})); g.appendChild(el('polyline',{points:polyPts(r.pts),fill:'none',stroke:'#d98a5b','stroke-width':.8,'stroke-dasharray':'1 7','stroke-linecap':'round',opacity:.9})); }); } function renderLines(){ const go=map.querySelector('#l-open'),gf=map.querySelector('#l-future'); go.innerHTML='';gf.innerHTML=''; go.style.display=state.layers.open?'':'none';gf.style.display=state.layers.future?'':'none'; LINES.forEach(L=>{ const pts=polyPts(L.pts); if(L.kind==="open"||L.kind==="train"){ if(!state.layers.open)return; const col=L.kind==="train"?"#5f7387":"#37b6a4"; go.appendChild(el('polyline',{points:pts,fill:'none',stroke:col,'stroke-width':L.kind==="train"?2:3.4,'stroke-linecap':'round','stroke-linejoin':'round',opacity:L.kind==="train"?.55:.9,filter:L.kind==="open"?'url(#glowF)':''})); }else{ if(!state.layers.future)return; gf.appendChild(el('polyline',{points:pts,fill:'none',stroke:'#6ea6ff','stroke-width':7,opacity:.10,'stroke-linecap':'round','stroke-linejoin':'round'})); const dash=el('polyline',{points:pts,fill:'none',stroke:'#6ea6ff','stroke-width':3.2,'stroke-linecap':'round','stroke-linejoin':'round','stroke-dasharray':'2 9',opacity:.95,filter:'url(#glowF)'}); dash.classList.add('flow');gf.appendChild(dash); const mid=L.pts[Math.floor(L.pts.length/2)];const[mx,my]=P(mid[0],mid[1]); const bg=el('g',{}); bg.appendChild(el('rect',{x:mx-15,y:my-19,width:30,height:13,rx:4,fill:'#0b1520',stroke:'#6ea6ff','stroke-width':.8,opacity:.95})); const bt=el('text',{x:mx,y:my-9,fill:'#6ea6ff','font-size':8.5,'text-anchor':'middle','font-family':'IBM Plex Mono'});bt.textContent=L.year; bg.appendChild(bt);gf.appendChild(bg); } }); } function glyph(g,t,x,y){ const col=t==="H"?"#e0a3d0":t==="R"?"#d9b46a":"#7fb0ff"; g.appendChild(el('circle',{cx:x,cy:y,r:6,fill:'#0b1520',stroke:col,'stroke-width':1.2,opacity:.95})); if(t==="H")g.appendChild(el('path',{d:`M ${x-2.6} ${y} h5.2 M ${x} ${y-2.6} v5.2`,stroke:col,'stroke-width':1.6,'stroke-linecap':'round'})); if(t==="R")g.appendChild(el('rect',{x:x-2.3,y:y-2.3,width:4.6,height:4.6,rx:1,fill:col})); if(t==="E")g.appendChild(el('path',{d:`M ${x} ${y-3} L ${x+2.8} ${y+2.4} L ${x-2.8} ${y+2.4} Z`,fill:col})); } function renderAmen(){ const g=map.querySelector('#l-amen');g.innerHTML=''; g.style.display=state.layers.amen?'':'none';if(!state.layers.amen)return; AMENITIES.forEach(a=>{const[x,y]=P(a.lat,a.lng);glyph(g,a.t,x,y);}); } function renderSchools(){ const g=map.querySelector('#l-schools');g.innerHTML=''; g.style.display=state.layers.schools?'':'none';if(!state.layers.schools)return; SCHOOLS.forEach(sch=>{ const[x,y]=P(sch.lat,sch.lng); g.appendChild(el('circle',{cx:x,cy:y,r:6,fill:'#0b1520',stroke:'#5fbf8a','stroke-width':1.1,opacity:.95})); let d="";for(let i=0;i<5;i++){const a=-Math.PI/2+i*2*Math.PI/5;const a2=a+Math.PI/5; d+=(i?'L':'M')+(x+Math.cos(a)*3.4)+' '+(y+Math.sin(a)*3.4)+' L '+(x+Math.cos(a2)*1.5)+' '+(y+Math.sin(a2)*1.5)+' ';} d+='Z';g.appendChild(el('path',{d,fill:'#5fbf8a'})); }); } function renderHeat(){ const g=map.querySelector('#l-heat');g.innerHTML=''; g.style.display=state.layers.heat?'':'none';if(!state.layers.heat)return; RANKED.forEach(r=>{ if(!hasComp(r.s))return; const[x,y]=P(r.s.lat,r.s.lng); g.appendChild(el('circle',{cx:x,cy:y,r:14+(r.score/100)*30,fill:'url(#heatg)',opacity:clamp((r.score-40)/60,.05,.85)})); }); } function renderWater(){map.querySelector('#water').style.display=state.layers.water?'':'none';} function renderMarkers(){ const g=map.querySelector('#l-markers');g.innerHTML=''; g.style.display=state.layers.score?'':'none';if(!state.layers.score)return; const budget=listingCap(); RANKED.forEach(r=>{ const s=r.s;const comp=hasComp(s); const[x,y]=P(s.lat,s.lng); const over=budget&&comp&&r.median>budget; const dimmed=(state.budgetDim&&over)||!comp; const gg=el('g',{class:'marker'});gg.dataset.id=s.id; if(dimmed)gg.setAttribute('opacity','.35'); const isSel=state.selected===s.id,isPin=state.pinned.includes(s.id); const rr=isSel?8.5:isPin?6.5:4.6; const col=comp?scoreColor(r.score):"#3a4a5a"; gg.appendChild(el('circle',{cx:x,cy:y,r:rr+3,fill:col,opacity:.14})); gg.appendChild(el('circle',{cx:x,cy:y,r:rr,fill:col,stroke:isSel?'#fff':'#0a0f16','stroke-width':isSel?1.6:1.3})); if(isSel||isPin)gg.appendChild(el('circle',{cx:x,cy:y,r:rr+4.5,fill:'none',stroke:col,'stroke-width':1,opacity:.7})); if(state.layers.labels&&(isSel||isPin||r.rank<=60||vt.k>=1.9)){ const lab=el('text',{x:x,y:y-rr-4,'text-anchor':'middle',class:'mk-lab'}); lab.textContent=s.name;if(isSel)lab.setAttribute('fill','#fff'); gg.appendChild(lab); } g.appendChild(gg); }); } function renderMap(){renderHeat();renderRoads();renderLines();renderAmen();renderSchools();renderWater();renderMarkers();} /* pan / zoom / hover */ let vt={x:0,y:0,k:1}; let lblZoom=false; function applyView(){map.querySelector('#view').setAttribute('transform',`translate(${vt.x} ${vt.y}) scale(${vt.k})`);const lz=vt.k>=1.9;if(lz!==lblZoom){lblZoom=lz;try{renderMarkers();}catch(e){}}} function zoomAt(cx,cy,factor){ const nk=clamp(vt.k*factor,1,5);const f=nk/vt.k; vt.x=cx-(cx-vt.x)*f;vt.y=cy-(cy-vt.y)*f;vt.k=nk; vt.x=clamp(vt.x,W-W*vt.k,0);vt.y=clamp(vt.y,H-H*vt.k,0);applyView(); } function svgPt(evt){ const r=map.getBoundingClientRect(); const vbAsp=W/H,boxAsp=r.width/r.height;let scale,offx=0,offy=0; if(boxAsp>vbAsp){scale=r.width/W;offy=(r.height-H*scale)/2;} else{scale=r.height/H;offx=(r.width-W*scale)/2;} return {mx:(evt.clientX-r.left-offx)/scale,my:(evt.clientY-r.top-offy)/scale}; } map.addEventListener('wheel',e=>{e.preventDefault();const{mx,my}=svgPt(e);zoomAt(mx,my,e.deltaY<0?1.15:1/1.15);},{passive:false}); document.getElementById('zin').onclick=()=>zoomAt(W/2,H/2,1.3); document.getElementById('zout').onclick=()=>zoomAt(W/2,H/2,1/1.3); document.getElementById('zreset').onclick=()=>{vt={x:0,y:0,k:1};applyView();}; let dragging=false,moved=false,last=null; map.addEventListener('pointerdown',e=>{dragging=true;moved=false;last={x:e.clientX,y:e.clientY};map.classList.add('drag');map.setPointerCapture(e.pointerId);}); map.addEventListener('pointermove',e=>{ if(dragging){ const dx=e.clientX-last.x,dy=e.clientY-last.y; if(Math.abs(dx)+Math.abs(dy)>3)moved=true; const r=map.getBoundingClientRect();const scale=Math.max(r.width/W,r.height/H); vt.x+=dx/scale;vt.y+=dy/scale;last={x:e.clientX,y:e.clientY}; vt.x=clamp(vt.x,W-W*vt.k,0);vt.y=clamp(vt.y,H-H*vt.k,0);applyView(); }else{handleHover(e.target.closest('.marker'),e);} }); map.addEventListener('pointerup',e=>{ dragging=false;map.classList.remove('drag'); if(!moved){const m=e.target.closest('.marker');if(m)selectSuburb(+m.dataset.id);} }); map.addEventListener('pointerleave',()=>{dragging=false;map.classList.remove('drag');hideTip();}); const tip=document.getElementById('tip'); function handleHover(m,e){ if(!m){hideTip();return;} const s=SUBURBS.find(x=>x.id===+m.dataset.id);const r=RANKED.find(x=>x.s.id===s.id); const fcCls=r.fc>=0?'up':'dn'; tip.innerHTML=`<div class="tn">${s.name}</div><div class="tr">${REGIONS[s.reg]} · #${r.rank}</div> <div class="trow"><span>Score</span><b style="color:${scoreColor(r.score)}">${Math.round(r.score)}</b></div> <div class="trow"><span>${state.type} median</span><b>${fmtMoney(r.median)}</b></div> <div class="trow"><span>12-mo f'cast</span><b class="fcv ${fcCls}">${pct(r.fc)}</b></div> <div class="trow"><span>Gross yield</span><b>${r.y!==null?r.y+'%':'—'}</b></div>`; const box=map.getBoundingClientRect(); let tx=e.clientX-box.left+14,ty=e.clientY-box.top+14; if(tx>box.width-190)tx=e.clientX-box.left-196; if(ty>box.height-130)ty=e.clientY-box.top-130; tip.style.left=tx+'px';tip.style.top=ty+'px';tip.classList.add('show'); } function hideTip(){tip.classList.remove('show');} /* ============================================================ EXPORT / COMPARE / METHODOLOGY / KEYBOARD ============================================================ */ function exportCSV(){ const rows=[["rank","suburb","postcode","region","score","grade","median_"+state.type,"gross_yield_pct","fc_12mo_pct","fc_3yr_pct","commute_min_to_"+state.workplace,"trend_12mo_pct","risk_0_100","cycle"]]; sortedRows().forEach(r=>{ rows.push([r.rank,r.s.name,r.s.pc,REGIONS[r.s.reg],Math.round(r.score),gradeOf(r.score)[0], r.median||"",r.y??"",r.fc.toFixed(1),r.fc3.toFixed(1),commuteMin(r.s),trend12(r.s),Math.round(r.risk*100),cycleStage(r.s)[0]]); }); const csv=rows.map(rw=>rw.map(v=>String(v).includes(',')?'"'+v+'"':v).join(",")).join("\n"); const a=document.createElement('a'); a.href=URL.createObjectURL(new Blob([csv],{type:'text/csv'})); a.download='meridian-ranking-'+state.type+'.csv';a.click(); setTimeout(()=>URL.revokeObjectURL(a.href),4000); } function openCompare(){ if(state.pinned.length<2)return; const rs=state.pinned.map(id=>RANKED.find(x=>x.s.id===id)).filter(Boolean); document.getElementById('cmpSub').textContent=rs.map(r=>r.s.name).join(' vs ')+' · '+state.type+'s'; const row=(label,vals,fmt)=>`<div style="display:grid;grid-template-columns:150px repeat(${rs.length},1fr);gap:8px;padding:7px 0;border-top:1px solid var(--line);align-items:center"> <div style="font-size:11px;color:var(--fog)">${label}</div>${vals.map(v=>`<div class="mono" style="font-size:12.5px;${fmt?fmt(v):''}">${v==null?'—':v}</div>`).join('')}</div>`; let html=`<div style="display:grid;grid-template-columns:150px repeat(${rs.length},1fr);gap:8px;padding:6px 0"> <div></div>${rs.map(r=>`<div style="font-family:var(--fs-display);font-weight:600">${r.s.name}</div>`).join('')}</div>`; const best=(arr,hiGood=true)=>{const nums=arr.map(v=>typeof v==='number'?v:-1e15);const b=hiGood?Math.max(...nums):Math.min(...nums.map(n=>n===-1e15?1e15:n));return v=>typeof v==='number'&&((hiGood&&v===b)||(!hiGood&&v===b))?'color:var(--amber-2);font-weight:600':'';}; let a; a=rs.map(r=>Math.round(r.score));html+=row("Score",a,best(a)); a=rs.map(r=>r.median?fmtMoney(r.median):null);html+=row("Median",a); a=rs.map(r=>+r.fc.toFixed(1));html+=row("12-mo forecast %",a,best(a)); a=rs.map(r=>+r.fc3.toFixed(1));html+=row("3-yr forecast %",a,best(a)); a=rs.map(r=>r.y);html+=row("Gross yield %",a,best(a)); a=rs.map(r=>commuteMin(r.s));html+=row("Commute (min)",a,best(a,false)); a=rs.map(r=>trend12(r.s));html+=row("12-mo trend* %",a,best(a)); a=rs.map(r=>Math.round(r.risk*100));html+=row("Risk load",a,best(a,false)); a=rs.map(r=>cycleStage(r.s)[0]);html+=row("Cycle stage",a); FACTORS.forEach(f=>{a=rs.map(r=>Math.round(clamp(factorRaw(r.s,f.k),0,100)));html+=row(f.label,a,best(a));}); document.getElementById('cmpTable').innerHTML=html; document.getElementById('cmpModal').classList.add('on'); } (function(){ const M=[ ["Composite score","Weighted mean of 8 factors (growth momentum, future transit, transport &amp; commute, gentrification, schools, zoning upside, everyday amenity, rental yield), each 0–100, using your slider weights — then risk-adjusted down by up to 16% based on supply pipeline (60%) and flood exposure (40%)."], ["Transport &amp; commute","55% rail quality + 45% a commute model: straight-line distance to your chosen work anchor at an effective speed set by rail quality (34/28/22 km/h) plus access overhead. Changing \"I commute to\" re-scores every suburb."], ["Forecasts","Each refresh samples one Big-4 2026 view (Westpac −3% → CBA +2%) plus an RBA rate-path (hold / cuts 2027 / one more hike), tilts it by suburb momentum, transit pipeline, gentrification and zoning, applies the current units-over-houses pattern, and subtracts a risk drag. 3-yr adds a rate-normalisation recovery."], ["Buying power","2025–26 tax + Medicare, HEM-style expense floor, 80% of rent counted, assessment at current variable + APRA 3% buffer over 30 yrs, then a bisection solve for the max price funding deposit, real NSW duty (FHB exemption/taper applied), LMI (waived under the First Home Guarantee ≤ $1.5M) and $3k costs."], ["Value estimate","Suburb median for your dwelling type × bed/bath/parking/land/condition multipliers, ±7% band. The verdict compares an asking price to that band."], ["After-tax cashflow","Investor mode: rent (1 wk vacancy) less interest, strata/rates, 6% management, insurance and a condition-based depreciation allowance; the net position is taxed/refunded at your marginal rate."], ["10-yr projection","Year 1 = current scenario forecast; years 2–3 = recovery path; years 4–10 = long-run rate from momentum + transit (2–7.5%). Equity = value minus the amortising P&amp;I balance."], ["Coverage &amp; data quality","369 suburbs across Greater Sydney. 75 flagship suburbs carry individually curated figures; the rest are modelled from district profiles calibrated to the same research anchors (each suburb shows a curated/modelled tag in its snapshot). Modelled medians are best treated as ±10% guides."], ["Limits","Suburb figures are calibrated estimates anchored to the dated sources (see Sources) — not live feeds. Always verify with comparable sales, strata records and Revenue NSW before acting."] ]; document.getElementById('methList').innerHTML=M.map(m=>`<div style="padding:9px 0;border-top:1px solid var(--line)"><b style="color:var(--paper)">${m[0]}</b><br>${m[1]}</div>`).join(''); })(); function renderShortlist(){ const box=document.getElementById('slList'); if(!state.shortlist.length){box.innerHTML='<div class="note" style="padding:8px 0">Nothing tracked yet — add the properties you actually inspect and Meridian will benchmark each against its suburb model (uses your current dwelling type & condition settings).</div>';return;} box.innerHTML=state.shortlist.map((e,i)=>{ const sub=SUBURBS.find(x=>x.id===e.sid);if(!sub)return ''; const r=RANKED.find(x=>x.s.id===e.sid); const est=estimatePrice(sub,e.bed); const rent=estimateRent(sub,e.bed); let vHtml='<span style="color:var(--fog)">no comparable model</span>'; if(est&&e.ask){ const gap=(e.ask-est)/est*100; const col=gap<-8?'#5fbf8a':gap<=6?'#37b6a4':gap<=15?'#eab34c':'#e5695c'; const lab=gap<-8?'Undervalued':gap<=6?'Fair value':gap<=15?'Slightly over':'Overpriced'; vHtml=`<b style="color:${col}">${lab}</b> <span class="mono" style="color:${col}">${(gap>=0?'+':'')+gap.toFixed(0)}%</span> vs model ${fmtMoney(est)}${rent?` · est $${rent}/wk (${(rent*52/e.ask*100).toFixed(1)}%)`:''}`; }else if(est){vHtml=`model ${fmtMoney(est)}${rent?` · est $${rent}/wk`:''}`;} return `<div style="border-top:1px solid var(--line);padding:10px 0;display:flex;gap:10px;align-items:center"> <div style="flex:1;min-width:0"> <div style="font-family:var(--fs-display);font-weight:600;font-size:13.5px">${e.addr}</div> <div style="font-size:11px;color:var(--fog);margin-top:2px">${sub.name} ${sub.pc} · ${e.bed}bd ${state.type} · suburb score <b style="color:${scoreColor(r?r.score:50)}">${r?Math.round(r.score):'—'}</b>${e.ask?` · asking ${fmtMoney(e.ask)}`:''}</div> <div style="font-size:11.5px;margin-top:4px">${vHtml}</div> </div> <a href="${reaURL(sub,e.bed)}" target="_blank" rel="noopener" style="color:var(--future);font-size:11px;text-decoration:none;flex:0 0 auto">listings ↗</a> <span data-sldel="${i}" style="color:var(--fog);cursor:pointer;font-size:15px;flex:0 0 auto">×</span> </div>`; }).join(''); } document.getElementById('slAdd').onclick=()=>{ const addr=document.getElementById('slAddr').value.trim(); const subName=document.getElementById('slSub').value.trim().toLowerCase(); const sub=SUBURBS.find(x=>x.name.toLowerCase()===subName)||SUBURBS.find(x=>subName&&x.name.toLowerCase().startsWith(subName)); if(!addr||!sub)return; state.shortlist.push({addr,sid:sub.id,ask:parseNum(document.getElementById('slAsk').value),bed:+document.getElementById('slBed').value}); document.getElementById('slAddr').value='';document.getElementById('slAsk').value=''; renderShortlist();renderPulse();save(); }; document.getElementById('slList').addEventListener('click',e=>{ const d=e.target.closest('[data-sldel]');if(!d)return; state.shortlist.splice(+d.dataset.sldel,1);renderShortlist();renderPulse();save(); }); document.getElementById('slClose').onclick=()=>document.getElementById('slModal').classList.remove('on'); document.getElementById('slModal').addEventListener('click',e=>{if(e.target.id==='slModal')e.currentTarget.classList.remove('on');}); document.getElementById('cmpClose').onclick=()=>document.getElementById('cmpModal').classList.remove('on'); document.getElementById('methClose').onclick=()=>document.getElementById('methModal').classList.remove('on'); ['cmpModal','methModal'].forEach(id=>document.getElementById(id).addEventListener('click',e=>{if(e.target.id===id)e.currentTarget.classList.remove('on');})); document.addEventListener('keydown',e=>{ if(e.key==='Escape'){ const open=document.querySelector('.modal.on'); if(open){open.classList.remove('on');return;} if(state.selected){document.getElementById('backBtn').click();} } if(e.key==='/'&&!/INPUT|TEXTAREA|SELECT/.test(document.activeElement.tagName)){ e.preventDefault();document.getElementById('search').focus(); } }); /* ============================================================ PULSE BAR ============================================================ */ function renderPulse(){ const p=document.getElementById('pulse'); const sc=FC.scenario; p.innerHTML=` <span>RBA <b>${MKT.cashRate.toFixed(2)}%</b></span><span class="sep">·</span> <span>SYD house <b>${fmtMoney(MKT.sydHouse)}</b> · unit <b>${fmtMoney(MKT.sydUnit)}</b></span><span class="sep">·</span> <span>Jun qtr <b class="dn">${MKT.juneQtr}%</b></span><span class="sep">·</span> <span>clearance <b>${MKT.clearance}%</b></span> <span class="tag">Buyers' market</span><span class="sep">·</span> <span><b>${SUBURBS.length}</b> suburbs</span><span class="sep">·</span> <span class="scen">Scenario: ${sc.anchor}-anchored, ${sc.pathLabel}</span> <button id="refreshFc" title="Resample forecast scenario across the researched forecaster spread"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"><path d="M21 12a9 9 0 1 1-2.6-6.4"/><path d="M21 3v6h-6"/></svg> <span class="bl">Refresh forecasts</span></button> <button class="ghost" id="slBtn" title="Properties you're tracking">★ <span class="bl">Shortlist (${state.shortlist.length})</span></button> <button class="ghost" id="csvBtn" title="Download the current ranking as CSV">⤓ <span class="bl">CSV</span></button> <button class="ghost" id="methBtn" title="How scoring, forecasting and finance are calculated">?<span class="bl"> Method</span></button> <button class="ghost" id="srcBtn">Sources · ${MKT.asOf}</button>`; document.getElementById('refreshFc').onclick=(e)=>{ const b=e.currentTarget;b.classList.add('spinning'); setTimeout(()=>b.classList.remove('spinning'),650); rollScenario(); }; document.getElementById('slBtn').onclick=()=>{renderShortlist();document.getElementById('slModal').classList.add('on');}; document.getElementById('csvBtn').onclick=exportCSV; document.getElementById('methBtn').onclick=()=>document.getElementById('methModal').classList.add('on'); document.getElementById('srcBtn').onclick=()=>document.getElementById('srcModal').classList.add('on'); } document.getElementById('srcClose').onclick=()=>document.getElementById('srcModal').classList.remove('on'); document.getElementById('srcModal').addEventListener('click',e=>{if(e.target.id==='srcModal')e.currentTarget.classList.remove('on');}); (function(){const l=document.getElementById('srcList'); l.innerHTML=SOURCES.map((s,i)=>`<div class="src"><div class="n">${String(i+1).padStart(2,'0')}</div><div class="t"><b>${s[0]}</b><span>${s[1]}</span></div></div>`).join('');})(); /* ============================================================ RANK LIST ============================================================ */ const rlist=document.getElementById('rlist'); function sortedRows(){ const rows=[...RANKED];const by=state.sort; if(by==="score")return rows; rows.sort((a,b)=>{ if(by==="price")return (a.median||9e15)-(b.median||9e15); if(by==="yield")return (b.y||0)-(a.y||0); if(by==="growth")return b.s.g-a.s.g; if(by==="future")return b.s.ft-a.s.ft; if(by==="gentr")return b.s.ge-a.s.ge; if(by==="fc")return b.fc-a.fc; if(by==="value")return (b.median?b.score/Math.sqrt(b.median):0)-(a.median?a.score/Math.sqrt(a.median):0); if(by==="commute")return commuteMin(a.s)-commuteMin(b.s); return 0;}); return rows; } function passFlt(r){ const f=state.flt; if(f.region!=='all'&&r.s.reg!==f.region)return false; if(f.q&&!r.s.name.toLowerCase().includes(f.q))return false; if(f.minY!=null&&(r.y==null||r.y<f.minY))return false; if(f.maxCm!=null&&commuteMin(r.s)>f.maxCm)return false; return true; } function renderRank(){ const rows=sortedRows().filter(passFlt); document.getElementById('rcount').textContent=rows.length+' / '+SUBURBS.length; rlist.innerHTML=''; const budget=listingCap(); rows.forEach(r=>{ const s=r.s;const comp=hasComp(s); const over=budget&&comp&&r.median>budget; const it=document.createElement('div'); it.className='ritem'+(state.selected===s.id?' sel':'')+((state.budgetDim&&over)||!comp?' dim':''); it.dataset.id=s.id; const futTag=s.lines.filter(l=>["MW","BK","WSA"].includes(l))[0]; const openTag=s.lines.some(l=>["M1","T","L"].includes(l)); const badge=futTag?`<span class="badge b-fut">${futTag==="MW"?"Metro West '32":futTag==="BK"?"Metro '26":"Airport '27"}</span>` :openTag?`<span class="badge b-open">on rail</span>`:""; const pinned=state.pinned.includes(s.id); const fcCls=r.fc>=0?'up':'dn'; it.innerHTML=` <div class="rk">${r.rank}</div> <div class="rmid"> <div class="rnm">${s.name} <svg class="pin ${pinned?'on':''}" data-pin="${s.id}" viewBox="0 0 24 24" fill="${pinned?'#eab34c':'none'}" stroke="${pinned?'#eab34c':'#7d92a8'}" stroke-width="2"><path d="M12 21s-7-5.2-7-11a7 7 0 0 1 14 0c0 5.8-7 11-7 11Z"/><circle cx="12" cy="10" r="2.4"/></svg> ${comp?`<a class="listlink" data-stop="1" href="${reaURL(s)}" target="_blank" rel="noopener" title="Open live listings for your brief"> <svg class="ext" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14 4h6v6M20 4 10 14"/><path d="M20 14v6H4V4h6"/></svg></a>`:''} </div> <div class="rmeta"><span>${fmtMoney(r.median)}</span>${comp?`<span class="fcv ${fcCls}">${pct(r.fc)} /yr</span>`:''}${r.y!==null?`<span>${r.y}% yld</span>`:''}${badge}</div> </div> <div class="rbar"><div class="bg"><div class="fl" style="width:${clamp(r.score,4,100)}%;background:${scoreColor(r.score)}"></div></div></div> <div class="rsc" style="color:${scoreColor(r.score)}">${comp?Math.round(r.score):'—'}</div>`; rlist.appendChild(it); }); } rlist.addEventListener('click',e=>{ if(e.target.closest('[data-stop]'))return; const pin=e.target.closest('[data-pin]'); if(pin){e.stopPropagation();togglePin(+pin.dataset.pin);return;} const it=e.target.closest('.ritem');if(it)selectSuburb(+it.dataset.id); }); document.getElementById('rsort').addEventListener('click',e=>{ const b=e.target.closest('button');if(!b)return; state.sort=b.dataset.sort; document.querySelectorAll('#rsort button').forEach(x=>x.classList.toggle('on',x===b)); renderRank();save(); }); (function(){ const rs=document.getElementById('regionSel'); rs.innerHTML='<option value="all">All regions</option>'+Object.entries(REGIONS).map(([k,v])=>`<option value="${k}">${v}</option>`).join(''); rs.onchange=()=>{state.flt.region=rs.value;renderRank();save();}; document.getElementById('rq').oninput=e=>{state.flt.q=e.target.value.trim().toLowerCase();renderRank();}; document.getElementById('minY').oninput=e=>{state.flt.minY=parseNum(e.target.value);renderRank();save();}; document.getElementById('maxCm').oninput=e=>{state.flt.maxCm=parseNum(e.target.value);renderRank();save();}; })(); /* ============================================================ DETAIL ============================================================ */ const detailView=document.getElementById('detailView'); const rankView=document.getElementById('rankView'); function gradeOf(v){ if(v>=82)return["A+","#f0894e"];if(v>=76)return["A","#eab34c"];if(v>=71)return["A−","#c9b24c"]; if(v>=66)return["B+","#5aa46a"];if(v>=60)return["B","#37b6a4"];if(v>=54)return["B−","#4a8fa8"]; return["C","#5f7387"]; } function radar(s){ const cx=110,cy=108,R=82;const n=FACTORS.length; let axes="",grid="",poly=""; for(let ring=1;ring<=3;ring++){ let p="";for(let i=0;i<n;i++){const a=-Math.PI/2+i*2*Math.PI/n;p+=(i?'L':'M')+(cx+Math.cos(a)*R*ring/3)+' '+(cy+Math.sin(a)*R*ring/3)+' ';} grid+=`<path d="${p}Z" fill="none" stroke="#26384a" stroke-width=".8" opacity=".6"/>`;} const pts=[]; FACTORS.forEach((f,i)=>{ const a=-Math.PI/2+i*2*Math.PI/n; const v=clamp(factorRaw(s,f.k),0,100)/100; axes+=`<line x1="${cx}" y1="${cy}" x2="${cx+Math.cos(a)*R}" y2="${cy+Math.sin(a)*R}" stroke="#26384a" stroke-width=".7" opacity=".5"/>`; const x=cx+Math.cos(a)*R*v,y=cy+Math.sin(a)*R*v;pts.push([x,y]); const lx=cx+Math.cos(a)*(R+13),ly=cy+Math.sin(a)*(R+13); axes+=`<circle cx="${x}" cy="${y}" r="2.4" fill="${f.color}"/>`; axes+=`<text x="${lx}" y="${ly+3}" text-anchor="middle" font-size="8" fill="#7d92a8" font-family="IBM Plex Mono">${f.label.split(' ')[0]}</text>`; }); poly=`<path d="M ${pts.map(p=>p.join(' ')).join(' L ')} Z" fill="rgba(234,179,76,.14)" stroke="#eab34c" stroke-width="1.6"/>`; return `<svg class="radar" width="220" height="216" viewBox="0 0 220 216">${grid}${axes}${poly}</svg>`; } function nearest(s,arr){ let best=null,bd=1e9; arr.forEach(a=>{const d=hav(s.lat,s.lng,a.lat,a.lng);if(d<bd){bd=d;best=a;}}); return {n:best.n,km:bd.toFixed(1)}; } function hav(la1,lo1,la2,lo2){ const R=6371,dLa=(la2-la1)*Math.PI/180,dLo=(lo2-lo1)*Math.PI/180; const a=Math.sin(dLa/2)**2+Math.cos(la1*Math.PI/180)*Math.cos(la2*Math.PI/180)*Math.sin(dLo/2)**2; return 2*R*Math.asin(Math.sqrt(a)); } function fcCard(s,r){ const y1=r.fc,sp=fcSpread(s),lo=y1-sp,hi=y1+sp; const dom=[-8,10]; const X=v=>clamp((v-dom[0])/(dom[1]-dom[0])*100,0,100); const col=y1>=2?"#5fbf8a":y1>=-1?"#eab34c":"#e5695c"; const scn=FC.scenario; return `<div class="fcard"> <div class="flabel">12-month outlook · ${state.type}s · this scenario</div> <div class="top"><span class="fnum" style="color:${col}">${pct(y1)}</span> <span class="frange">range ${pct(lo)} → ${pct(hi)}</span></div> <div class="fbar"> <div class="axis"></div> <div class="zero" style="left:${X(0)}%"></div> <div class="band" style="left:${X(lo)}%;width:${X(hi)-X(lo)}%;background:${hexA(col,.28)}"></div> <div class="pt" style="left:${X(y1)}%;background:${col}"></div> </div> <div class="ticks"><span>−8%</span><span>0</span><span>+10%</span></div> <div class="three"><span>3-yr cumulative (if cuts normalise)</span><b style="color:${r.fc3>=0?'#5fbf8a':'#e5695c'}">${pct(r.fc3)}</b></div> <div class="note" style="margin-top:9px">Anchored to the ${scn.anchor} 2026 view within the Big-4 spread (Westpac −3% → CBA +2%), path: ${scn.pathLabel}; tilted by this suburb's momentum, transit pipeline, gentrification &amp; supply risk. Hit ↻ in the top bar to resample.</div> </div>`; } function selectSuburb(id){ state.selected=id;refreshDetail(); rankView.style.display='none';detailView.classList.add('on'); renderRank();renderMarkers(); if(window.matchMedia('(max-width:980px)').matches)setMobile('rank'); } function refreshDetail(){ const id=state.selected; const r=RANKED.find(x=>x.s.id===id);if(!r)return; const s=r.s; const[grade,gcol]=gradeOf(r.score); document.getElementById('dName').textContent=s.name; const[cyc,cycCol]=cycleStage(s); document.getElementById('dRegion').innerHTML=REGIONS[s.reg]+' · '+s.pc+ ` · <span style="color:${cycCol};border:1px solid ${hexA(cycCol,.45)};padding:1px 6px;border-radius:5px;font-size:9px">${cyc}</span>`; document.getElementById('dGrade').textContent=grade;document.getElementById('dGrade').style.color=gcol; document.getElementById('dScoreNum').textContent=Math.round(r.score); document.getElementById('dScoreNum').style.color=scoreColor(r.score); const fill=document.getElementById('dScoreFill');fill.style.width=clamp(r.score,4,100)+'%';fill.style.background=scoreColor(r.score); document.getElementById('dRank').textContent='#'+r.rank+' of '+SUBURBS.length; const lineMeta={MW:["Metro West","2032","#6ea6ff"],BK:["Southwest Metro","2026","#6ea6ff"],WSA:["Airport Line","2027","#6ea6ff"], M1:["M1 Metro","open","#37b6a4"],T:["Sydney Trains","open","#37b6a4"],L:["Light Rail","open","#37b6a4"]}; let hl=""; s.lines.forEach(l=>{const m=lineMeta[l];if(!m)return; hl+=`<div class="row"><div class="ic" style="background:rgba(110,166,255,.14)"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="${m[2]}" stroke-width="2"><rect x="5" y="3" width="14" height="14" rx="3"/><path d="M8 21l1.5-3M16 21l-1.5-3M9 11h6"/></svg></div> <b>${m[0]}</b><span class="yr" style="${m[1]==='open'?'background:rgba(55,182,164,.14);color:#37b6a4':''}">${m[1]}</span></div>`;}); const nA=nearest(s,AMENITIES),nS=nearest(s,SCHOOLS); hl+=`<div class="row"><div class="ic" style="background:rgba(224,163,208,.14)"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#e0a3d0" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg></div> <b>${nA.n}</b><span style="margin-left:auto;color:#7d92a8;font-size:11px" class="mono">${nA.km} km</span></div>`; hl+=`<div class="row"><div class="ic" style="background:rgba(95,191,138,.14)"> <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="#5fbf8a" stroke-width="2"><path d="M3 9l9-5 9 5-9 5-9-5Z"/><path d="M7 11v4c0 1 2.2 2.5 5 2.5s5-1.5 5-2.5v-4"/></svg></div> <b>${nS.n}</b><span style="margin-left:auto;color:#7d92a8;font-size:11px" class="mono">${nS.km} km</span></div>`; const cheaper=RANKED.filter(x=>x.s.reg===s.reg&&x.median>0&&x.median<r.median*0.92&&x.score>=r.score-6) .sort((a,b)=>b.score-a.score)[0]; const risk=Math.round(r.risk*100); const facts=FACTORS.map(f=>{ const v=clamp(factorRaw(s,f.k),0,100); return `<div class="fact"><div class="fl"><span class="dot" style="background:${f.color}"></span>${f.label}</div> <div class="tr"><div class="f" style="width:${v}%;background:${f.color}"></div></div><div class="fv">${Math.round(v)}</div></div>`;}).join(''); const cap=listingCap(); const capSrc=state.fin.buyPower?'your buying power':state.budget?'your budget':null; const cmn=commuteMin(s); const t12=trend12(s); const comps=RANKED.filter(x=>x.median>0); const pctile=Math.round(comps.filter(x=>x.median<=r.median).length/Math.max(1,comps.length)*100); document.getElementById('dBody').innerHTML=` <div class="dsec"> <h3>Forecast</h3> ${fcCard(s,r)} </div> <div class="dsec"> <h3>Live listings <span class="r">${state.bed}bd ${state.type}${cap?' ≤ '+fmtMoney(cap):''}</span></h3> <div class="lbtns" style="margin-top:2px"> <a class="lbtn" href="${reaURL(s)}" target="_blank" rel="noopener"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M3 11 12 3l9 8"/><path d="M5 10v10h14V10"/></svg>realestate.com.au</a> <a class="lbtn" href="${domainURL(s)}" target="_blank" rel="noopener"> <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="9"/><path d="M8 12h8M12 8v8"/></svg>Domain</a> </div> <div class="lcap">${cap?`Filtered to ${capSrc} (${fmtMoney(cap)}), ${state.bed} bed, ${state.type} in ${s.name} ${s.pc} — live results.`:`Set a deposit or budget on the left to auto-cap these searches to what you can afford.`}</div> </div> <div class="dsec"> <h3>Factor profile</h3> ${radar(s)} <div style="margin-top:6px">${facts}</div> </div> <div class="dsec"> <h3>Market snapshot · ${state.type}</h3> <div class="kv"> <div class="cell"><div class="k">Median</div><div class="v">${fmtMoney(r.median)}</div></div> <div class="cell"><div class="k">Gross yield</div><div class="v">${r.y!==null?r.y+'<small>%</small>':'—'}</div></div> <div class="cell"><div class="k">To ${ANCHORS[state.workplace].n.split(' ')[0]}</div><div class="v">≈${cmn}<small> min</small></div></div> <div class="cell"><div class="k">12-mo trend*</div><div class="v" style="color:${t12>=0?'#5fbf8a':'#e5695c'}">${pct(t12)}</div></div> <div class="cell"><div class="k">Vacancy</div><div class="v">${s.vac}<small>%</small></div></div> <div class="cell"><div class="k">Risk load</div><div class="v" style="color:${risk>60?'#e5695c':risk>40?'#eab34c':'#5fbf8a'}">${risk}<small>/100</small></div></div> <div class="cell"><div class="k">Price rank</div><div class="v">P${pctile}<small> of SYD</small></div></div> <div class="cell"><div class="k">Data</div><div class="v" style="font-size:12px;color:${s.id<=75?'var(--amber-2)':'var(--fog)'}">${s.id<=75?'curated':'modelled'}</div></div> </div> <div class="narr" style="margin-top:12px">${s.note}</div> <div class="note" style="margin-top:8px">*modelled from momentum score, calibrated to Cotality SA3 growth anchors.</div> </div> <div class="dsec"> <h3>Location catalysts</h3> <div class="hl">${hl}</div> ${cheaper?`<div class="note" style="margin-top:11px">↳ Ripple play: <b style="color:var(--amber-2)">${cheaper.s.name}</b> scores ${Math.round(cheaper.score)} at ${fmtMoney(cheaper.median)} — a cheaper entry in the same corridor.</div>`:''} </div> <div class="dsec"> <h3>Value &amp; cashflow</h3> <div class="estgrid"> <div class="field"><label>Asking price (optional)</label> <div class="money"><input id="askIn" inputmode="numeric" placeholder="e.g. 950000" value="${state.asking||''}"></div></div> <div class="field"><label>Your brief</label> <select class="inp" disabled><option>${state.bed}bd · ${state.bath}ba · ${state.car} car · ${state.type}</option></select></div> <div class="field"><label>Loan type</label> <select class="inp" id="ioSel"><option value="pi"${state.io?'':' selected'}>Principal &amp; interest</option><option value="io"${state.io?' selected':''}>Interest-only (5yr)</option></select></div> <div class="field"><label>Strata+rates $/mo (override)</label> <div class="money"><input id="strataIn" inputmode="numeric" placeholder="auto" value="${state.strataOv||''}"></div></div> </div> <div id="estBox"></div> <div class="note" style="margin-top:10px">Estimate = suburb median for your dwelling type adjusted for beds, baths, parking${state.type==='house'?', land':''} and condition; cashflow at ${(state.fin.who==='inv'?MKT.varINV:MKT.varOO).toFixed(2)}% on your computed loan structure. Triangulate against live comparable sales before offering.</div> </div> <div class="dsec"> <h3>10-year projection <span class="r">value ··· equity —</span></h3> <div id="eqBox"></div> </div> <div class="dsec"> <h3>My notes <span class="r"><button id="copyBrief" style="background:var(--ink-3);border:1px solid var(--line);border-radius:6px;padding:3px 9px;font-size:10px;color:var(--amber-2);font-family:var(--fs-mono)">Copy brief ⧉</button></span></h3> <textarea id="noteBox" placeholder="Inspection impressions, agents, comparable sales, strata report flags…" style="width:100%;min-height:74px;background:var(--ink-3);border:1px solid var(--line);border-radius:9px;padding:9px 11px;font-size:12px;color:var(--paper);resize:vertical;outline:0;font-family:var(--fs-body)">${(state.notes[s.id]||'').replace(/</g,'&lt;')}</textarea> </div>`; renderEstimate(); const askIn=document.getElementById('askIn'); askIn.addEventListener('input',()=>{state.asking=parseNum(askIn.value);renderEstimate();}); document.getElementById('ioSel').addEventListener('change',e=>{state.io=e.target.value==='io';save();renderEstimate();}); document.getElementById('strataIn').addEventListener('change',e=>{state.strataOv=parseNum(e.target.value);save();renderEstimate();}); const noteBox=document.getElementById('noteBox'); noteBox.addEventListener('input',()=>{state.notes[s.id]=noteBox.value;save();}); document.getElementById('copyBrief').addEventListener('click',e=>{ const txt=`${s.name} ${s.pc} (${REGIONS[s.reg]}) — Meridian brief ${MKT.asOf} Score ${Math.round(r.score)}/100 (#${r.rank} of ${SUBURBS.length}) · ${cyc} ${state.type} median ${fmtMoney(r.median)} · gross yield ${r.y??'—'}% · vacancy ${s.vac}% 12-mo forecast ${pct(r.fc)} (3-yr ${pct(r.fc3)}) · modelled 12-mo trend ${pct(t12)} Commute ≈${cmn} min to ${ANCHORS[state.workplace].n} · risk ${risk}/100 ${s.note}${state.notes[s.id]?'\nNotes: '+state.notes[s.id]:''}`; navigator.clipboard.writeText(txt).then(()=>{e.target.textContent='Copied ✓';setTimeout(()=>e.target.textContent='Copy brief ⧉',1400);}); }); } document.getElementById('backBtn').onclick=()=>{ state.selected=null;detailView.classList.remove('on');rankView.style.display='';renderRank();renderMarkers(); }; /* estimator + cashflow */ function estimatePrice(s,bedOv){ const bed=bedOv||state.bed; let base=medianFor(s);if(!base)return null; const bedTable=state.type==="house"||state.type==="townhouse"?{1:0.72,2:0.83,3:1.0,4:1.17,5:1.32}:{1:0.79,2:1.0,3:1.25,4:1.48,5:1.66}; const bF=bedTable[bed]||1; const baF=1+(state.bath-1)*0.06; const cF={0:0.94,1:1,2:1.06,3:1.10}[state.car]||1; const lF=state.type==="house"?state.land:(state.type==="townhouse"?1+(state.land-1)*0.4:1); return base*bF*baF*cF*lF*state.cond; } function estimateRent(s,bedOv){ const bed=bedOv||state.bed; let base=state.type==="house"?s.rh:state.type==="townhouse"?Math.round(s.rh*0.55+s.ru*0.6):s.ru; if(!base)return null; const bedTable=state.type==="house"||state.type==="townhouse"?{1:0.72,2:0.83,3:1.0,4:1.15,5:1.25}:{1:0.8,2:1.0,3:1.22,4:1.4,5:1.5}; return Math.round(base*(bedTable[bed]||1)*state.cond*(1+(state.bath-1)*0.04)); } function renderEstimate(){ const s=SUBURBS.find(x=>x.id===state.selected);if(!s)return; const box=document.getElementById('estBox');if(!box)return; const est=estimatePrice(s); if(!est){box.innerHTML=`<div class="estout"><div class="lab">Estimated value</div><div class="big">No comparable ${state.type} market</div><div class="rng">${s.name} has limited ${state.type} sales — try a different dwelling type.</div></div>`;const eb=document.getElementById('eqBox');if(eb)eb.innerHTML='<div class="note">Projection needs a comparable market for this dwelling type.</div>';return;} const lo=est*0.93,hi=est*1.07; const rent=estimateRent(s); const grossY=rent?+(rent*52/est*100).toFixed(1):null; const rent3=rent?Math.round(rent*Math.pow(1+MKT.rentG/100,3)):null; // SQM/Domain forward guidance ~2-5%/yr // cashflow at real rates using user's actual deposit if provided let cash=""; const price=state.asking||est; const dep=state.fin.dep; const fhbOwn=state.fin.fhb&&state.fin.who==="oo"; const duty=dutyFor(price,fhbOwn); const loan=dep?Math.max(0,price+duty+MKT.buyCosts-dep):price*0.8; const rate=(state.fin.who==="inv"?MKT.varINV:MKT.varOO)+(state.io?0.25:0); if(rent){ const mrep=state.io?loan*rate/1200:repay(loan,rate); const strata=state.strataOv!=null?state.strataOv:(state.type==="unit"?Math.round(price*0.006/12+90):120); const mgmt=state.fin.who==="inv"?rent*0.06:0; const net=(rent*52/12)-mrep-strata-mgmt; const wk=Math.round(net*12/52); cash=`<div class="yieldline"><span>Repayment (${rate.toFixed(2)}% ${state.io?'IO':'P&I'}, loan ${fmtMoney(loan)})</span><b>${fmtFull(mrep)}/mo</b></div> <div class="yieldline" style="border-top:0;margin-top:4px;padding-top:0"><span>Pre-tax cashflow after rent, strata${state.fin.who==='inv'?' &amp; mgmt':''}</span><b style="color:${wk>=0?'#5fbf8a':'#e5695c'}">${wk>=0?'+':''}$${wk}/wk</b></div>`; if(state.fin.who==="inv"&&state.fin.inc){ const mRate=marginalRate(state.fin.inc); const depr=state.cond>=1.18?8000:state.cond>=1.1?4000:1500; // capital works + plant, rough const annRent=rent*52*0.98; // 1 wk vacancy allowance const deductions=loan*rate/100+strata*12+annRent*0.06+600+depr; const taxable=annRent-deductions; const taxFx=taxable<0?-taxable*mRate:-(taxable*mRate); const atWk=Math.round((net*12+(taxable<0?-taxable*mRate:-taxable*mRate))/52); cash+=`<div class="yieldline" style="border-top:0;margin-top:4px;padding-top:0"><span>After tax at your ${Math.round(mRate*100)}% marginal rate${taxable<0?' (neg. geared)':''}</span><b style="color:${atWk>=0?'#5fbf8a':'#e5695c'}">${atWk>=0?'+':''}$${atWk}/wk</b></div>`; } cash+=`<div class="yieldline" style="border-top:0;margin-top:4px;padding-top:0"><span>Stamp duty at this price${fhbOwn?' (FHB)':''}</span><b>${fmtFull(duty)}</b></div> <div class="yieldline" style="border-top:0;margin-top:4px;padding-top:0"><span>Est. rent in 3 yrs</span><b>$${rent3}/wk</b></div>`; } let verdict=""; if(state.asking){ const gap=(state.asking-est)/est*100; let vt,vs,col,ic; if(gap<-8){vt="Undervalued";vs=`Asking is ${Math.abs(gap).toFixed(0)}% below the modelled fair value — and it's a buyers' market.`;col="#5fbf8a";ic="↓";} else if(gap<=6){vt="Fair value";vs=`Asking sits within the modelled range (${(gap>=0?'+':'')}${gap.toFixed(0)}%). With ~51% clearance, still negotiate.`;col="#37b6a4";ic="≈";} else if(gap<=15){vt="Slightly over";vs=`Asking is ${gap.toFixed(0)}% above fair value — clear room to negotiate in this market.`;col="#eab34c";ic="↑";} else{vt="Overpriced";vs=`Asking is ${gap.toFixed(0)}% above the modelled range.`;col="#e5695c";ic="↑↑";} const askY=rent?+(rent*52/state.asking*100).toFixed(1):null; verdict=`<div class="verdict" style="background:${hexA(col,.10)};border:1px solid ${hexA(col,.4)}"> <div class="vic" style="background:${hexA(col,.18)};color:${col}">${ic}</div> <div><div class="vt" style="color:${col}">${vt}</div><div class="vs">${vs}</div></div></div> ${askY?`<div class="yieldline"><span>Gross yield at asking</span><b style="color:${col}">${askY}%</b></div>`:''}`; } box.innerHTML=` <div class="estout"> <div class="lab">Estimated fair value · ${state.bed}bd ${state.type}</div> <div class="big">${fmtFull(est)}</div> <div class="rng">Range ${fmtFull(lo)} – ${fmtFull(hi)}</div> ${rent?`<div class="yieldline"><span>Est. weekly rent · gross yield</span><b>$${rent}/wk · ${grossY}%</b></div>`:''} ${cash} </div>${verdict}`; const eb=document.getElementById('eqBox'); if(eb){ const r=RANKED.find(x=>x.s.id===s.id); eb.innerHTML=eqChart(s,r,price,loan)+ `<div class="note" style="margin-top:6px">Buy at ${fmtMoney(price)} with loan ${fmtMoney(loan)} (P&amp;I amortised): year-1 uses this scenario's forecast, years 2–3 the recovery path, then a long-run rate from momentum + transit pipeline. Dotted amber = value, green = your equity.</div>`; } } /* ============================================================ PIN / COMPARE ============================================================ */ const cmptray=document.getElementById('cmptray'); function togglePin(id){ const i=state.pinned.indexOf(id); if(i>=0)state.pinned.splice(i,1); else{if(state.pinned.length>=3)state.pinned.shift();state.pinned.push(id);} renderCompare();renderRank();renderMarkers();save(); } function renderCompare(){ if(!state.pinned.length){cmptray.classList.add('hide');return;} cmptray.classList.remove('hide'); let html=`<div class="cmphint">Compare (${state.pinned.length}/3)</div>`; state.pinned.forEach(id=>{ const r=RANKED.find(x=>x.s.id===id);const s=r.s; html+=`<div class="cmpcard" data-cid="${id}"><span class="x" data-x="${id}">×</span> <div class="cn">${s.name}</div> <div class="cs">Score <b>${Math.round(r.score)}</b> · ${fmtMoney(r.median)} · ${pct(r.fc)}</div></div>`;}); if(state.pinned.length>=2)html+=`<button id="cmpOpen" style="background:var(--amber);border:0;border-radius:8px;padding:0 13px;font-weight:600;font-size:12px;color:#191203;align-self:stretch">Compare ⇄</button>`; cmptray.innerHTML=html; const co=document.getElementById('cmpOpen');if(co)co.onclick=openCompare; } cmptray.addEventListener('click',e=>{ const x=e.target.closest('[data-x]');if(x){togglePin(+x.dataset.x);return;} const c=e.target.closest('[data-cid]');if(c)selectSuburb(+c.dataset.cid); }); /* ============================================================ CONTROLS ============================================================ */ function buildWeights(){ const wrap=document.getElementById('weights');wrap.innerHTML=''; FACTORS.forEach(f=>{ const row=document.createElement('div');row.className='wrow'; row.innerHTML=`<div class="wl"><span class="dot" style="background:${f.color}"></span>${f.label}</div> <input type="range" min="0" max="30" value="${state.weights[f.k]}" data-w="${f.k}"> <div class="wv" data-wv="${f.k}">${state.weights[f.k]}</div>`; wrap.appendChild(row);}); wrap.oninput=e=>{ const k=e.target.dataset.w;if(!k)return; state.weights[k]=+e.target.value; document.querySelector(`[data-wv="${k}"]`).textContent=e.target.value; recompute();}; } function buildLayers(){ const wrap=document.getElementById('layers');wrap.innerHTML=''; LAYER_DEFS.forEach(l=>{ const t=document.createElement('label');t.className='toggle'+(state.layers[l.k]?' on':'');t.dataset.layer=l.k; let sw=l.kind==='gl'?`<span class="gl" style="background:${hexA(l.color,.7)}"></span>` :`<span class="swatch ${l.kind==='dash'?'dash':''}" style="border-color:${l.color}"></span>`; t.innerHTML=`<span class="sw"></span><span class="tl">${l.label}</span>${sw}`; wrap.appendChild(t);}); wrap.onclick=e=>{ const t=e.target.closest('[data-layer]');if(!t)return; const k=t.dataset.layer;state.layers[k]=!state.layers[k]; t.classList.toggle('on',state.layers[k]);renderMap();save();}; } function setStrategy(strat){ state.strategy=strat;state.weights={...PRESETS[strat]}; document.querySelectorAll('#strategySeg button').forEach(b=>b.classList.toggle('on',b.dataset.strat===strat)); document.getElementById('stratLabel').textContent={balanced:"Balanced",growth:"Capital growth",yield:"Rental yield"}[strat]; buildWeights();recompute(); } document.getElementById('strategySeg').onclick=e=>{const b=e.target.closest('button');if(b)setStrategy(b.dataset.strat);}; document.getElementById('resetW').onclick=()=>{state.weights={...PRESETS[state.strategy]};buildWeights();recompute();}; function wireChips(id,key,cast){ document.getElementById(id).onclick=e=>{ const c=e.target.closest('.chip');if(!c)return; document.querySelectorAll(`#${id} .chip`).forEach(x=>x.classList.toggle('on',x===c)); state[key]=cast(c.dataset[key]); if(key==='type')document.getElementById('landField').style.display=(state.type==='house'||state.type==='townhouse')?'':'none'; recompute();}; } wireChips('typeChips','type',v=>v); wireChips('bedChips','bed',v=>+v); wireChips('bathChips','bath',v=>+v); wireChips('carChips','car',v=>+v); document.getElementById('landSel').onchange=e=>{state.land=+e.target.value;save();if(state.selected)renderEstimate();}; document.getElementById('condSel').onchange=e=>{state.cond=+e.target.value;save();if(state.selected)renderEstimate();}; document.getElementById('workSel').onchange=e=>{state.workplace=e.target.value;recompute();}; const budgetIn=document.getElementById('budget'); budgetIn.oninput=()=>{state.budget=parseNum(budgetIn.value);updateBudgetNote();renderRank();renderMarkers();if(state.selected)refreshDetail();save();}; document.getElementById('budgetTog').onclick=()=>{ state.budgetDim=!state.budgetDim; document.getElementById('budgetTog').classList.toggle('on',state.budgetDim); renderRank();renderMarkers();save();}; function updateBudgetNote(){ const n=document.getElementById('budgetNote'); const cap=listingCap(); if(!cap){n.textContent="Enter a budget — or fill the deposit section above and it flows through automatically.";return;} const within=RANKED.filter(r=>hasComp(r.s)&&r.median<=cap).length; const src=state.fin.buyPower&&(!state.budget||state.fin.buyPower===state.budget)?"buying power":"budget"; n.innerHTML=`<b style="color:var(--amber-2)">${within}</b> of ${SUBURBS.length} suburbs have a ${state.type} median at or under your ${src} of ${fmtMoney(cap)}.`; } /* finance wiring */ function wireFin(){ const ids={fDep:'dep',fInc:'inc',fInc2:'inc2',fExp:'exp',fDebt:'debt',fRent:'rent'}; Object.entries(ids).forEach(([id,k])=>{ document.getElementById(id).oninput=e=>{state.fin[k]=parseNum(e.target.value)||(k==='debt'?0:null);finRecalc();};}); document.getElementById('fKids').onchange=e=>{state.fin.kids=+e.target.value;finRecalc();}; document.getElementById('fWho').onchange=e=>{state.fin.who=e.target.value; document.getElementById('rentField').style.display=state.fin.who==='inv'?'':'none';finRecalc();}; document.getElementById('fFhb').onchange=e=>{state.fin.fhb=e.target.value==='1';finRecalc();}; document.getElementById('assessNote').textContent=(MKT.varOO+MKT.buffer).toFixed(2)+'%'; } function finRecalc(){ solveBuyPower(); renderBP(); updateBudgetNote();renderRank();renderMarkers(); if(state.selected)refreshDetail(); save(); } function renderBP(){ const out=document.getElementById('bpOut'); const f=state.fin,d=f.detail; if(!f.buyPower||!d){ out.innerHTML=`<div class="bp"><div class="lab">Estimated buying power</div> <div class="big" style="color:var(--fog)">—</div> <div class="sub2">Enter your deposit and income to size your ceiling.</div></div>`; return; } // stress test: what happens if rates rise another 1% let stress=null; {const oOO=MKT.varOO,oINV=MKT.varINV; MKT.varOO+=1;MKT.varINV+=1; const keepBP=f.buyPower,keepD=f.detail; solveBuyPower();stress=f.buyPower; MKT.varOO=oOO;MKT.varINV=oINV; f.buyPower=keepBP;f.detail=keepD;} const fhg=d.fhg; const flags=[]; if(f.fhb&&f.who==="oo"&&f.buyPower<=MKT.fhbExempt)flags.push(`<span class="flag">✓ $0 stamp duty (FHBAS ≤ $800k)</span>`); else if(f.fhb&&f.who==="oo"&&f.buyPower<MKT.fhbTaper)flags.push(`<span class="flag">✓ FHB duty concession applies</span>`); if(fhg)flags.push(`<span class="flag">✓ First Home Guarantee: 5% deposit, no LMI</span>`); else if(d.lvr>0.8)flags.push(`<span class="flag warn">LMI ≈ ${fmtFull(d.lmi)} capitalised (LVR ${(d.lvr*100).toFixed(0)}%)</span>`); out.innerHTML=`<div class="bp"> <div class="lab">Estimated buying power</div> <div class="big">${fmtMoney(f.buyPower)}</div> <div class="sub2">deposit ${fmtMoney(f.dep)} + loan ${fmtMoney(d.loan)} · LVR ${(d.lvr*100).toFixed(0)}%</div> <div class="rows"> <div class="r"><span>Serviceable loan cap</span><b>${fmtMoney(d.maxLoan)}</b></div> <div class="r"><span>Stamp duty at ceiling</span><b>${fmtFull(d.duty)}</b></div> <div class="r"><span>Repayment at ${(f.who==='inv'?MKT.varINV:MKT.varOO).toFixed(2)}%</span><b>${fmtFull(d.repayM)}/mo</b></div> ${stress!=null?`<div class="r"><span>Stress: ceiling if rates +1%</span><b style="color:var(--coral)">${fmtMoney(stress)}</b></div>`:''} </div> ${flags.join('')} <button id="useBP">Use as my budget filter →</button> </div>`; document.getElementById('useBP').onclick=()=>{ state.budget=state.fin.buyPower; budgetIn.value=state.fin.buyPower; updateBudgetNote();renderRank();renderMarkers();if(state.selected)refreshDetail(); }; } /* collapsible sections */ document.querySelectorAll('[data-collapse]').forEach(h=>{ h.onclick=()=>document.getElementById(h.dataset.collapse).classList.toggle('collapsed'); }); /* search + mobile */ document.getElementById('search').onchange=e=>{ const q=e.target.value.trim().toLowerCase();if(!q)return; let hit=SUBURBS.find(s=>s.name.toLowerCase()===q)||SUBURBS.find(s=>q.includes(s.name.toLowerCase()))||SUBURBS.find(s=>s.name.toLowerCase().startsWith(q)); if(hit){selectSuburb(hit.id);centerOn(hit);e.target.value=hit.name;} }; function centerOn(s){ const[x,y]=P(s.lat,s.lng);vt.k=2.2;vt.x=W/2-x*vt.k;vt.y=H/2-y*vt.k; vt.x=clamp(vt.x,W-W*vt.k,0);vt.y=clamp(vt.y,H-H*vt.k,0);applyView(); } (function(){document.getElementById('sublist').innerHTML=SUBURBS.map(s=>`<option value="${s.name}">`).join('');})(); function setMobile(v){ document.getElementById('bodyGrid').className='body view-'+v; document.querySelectorAll('#tabbar button').forEach(b=>b.classList.toggle('on',b.dataset.view===v)); } document.getElementById('tabbar').onclick=e=>{const b=e.target.closest('button');if(b)setMobile(b.dataset.view);}; /* ============================================================ PIPELINE + INIT ============================================================ */ function recompute(){ RANKED=computeAll(); renderRank();renderMap();renderCompare();updateBudgetNote(); if(state.selected&&detailView.classList.contains('on'))refreshDetail(); save(); } function reflectUI(){ document.querySelectorAll('#typeChips .chip').forEach(c=>c.classList.toggle('on',c.dataset.type===state.type)); document.querySelectorAll('#bedChips .chip').forEach(c=>c.classList.toggle('on',+c.dataset.bed===state.bed)); document.querySelectorAll('#bathChips .chip').forEach(c=>c.classList.toggle('on',+c.dataset.bath===state.bath)); document.querySelectorAll('#carChips .chip').forEach(c=>c.classList.toggle('on',+c.dataset.car===state.car)); document.getElementById('landSel').value=String(state.land); document.getElementById('condSel').value=String(state.cond); document.getElementById('workSel').value=state.workplace; document.getElementById('landField').style.display=(state.type==='house'||state.type==='townhouse')?'':'none'; if(state.budget)budgetIn.value=state.budget; document.getElementById('budgetTog').classList.toggle('on',state.budgetDim); const f=state.fin; const m={fDep:f.dep,fInc:f.inc,fInc2:f.inc2,fExp:f.exp,fDebt:f.debt||null,fRent:f.rent}; Object.entries(m).forEach(([id,v])=>{if(v)document.getElementById(id).value=v;}); document.getElementById('fKids').value=String(f.kids||0); document.getElementById('fWho').value=f.who; document.getElementById('fFhb').value=f.fhb?'1':'0'; document.getElementById('rentField').style.display=f.who==='inv'?'':'none'; document.querySelectorAll('#strategySeg button').forEach(b=>b.classList.toggle('on',b.dataset.strat===state.strategy)); document.getElementById('stratLabel').textContent={balanced:"Balanced",growth:"Capital growth",yield:"Rental yield"}[state.strategy]; document.querySelectorAll('#rsort button').forEach(b=>b.classList.toggle('on',b.dataset.sort===state.sort)); document.getElementById('regionSel').value=state.flt.region; if(state.flt.minY)document.getElementById('minY').value=state.flt.minY; if(state.flt.maxCm)document.getElementById('maxCm').value=state.flt.maxCm; } function init(){ load(); rollScenario(true); buildBase();buildWeights();buildLayers();wireFin();reflectUI(); document.getElementById('bodyGrid').className='body view-map'; solveBuyPower(); RANKED=computeAll(); renderPulse();renderMap();renderRank();renderCompare();updateBudgetNote();renderBP(); applyView(); } init(); </script> </body> </html>

Drag to resize
Drag to resize
Drag to resize
Drag to resize