# Vibe coding an EV charging map out of Australian open data

<style>
  :root{
    --ink:#15181d; --ink-2:#414a56; --ink-3:#727d8c;
    --paper:#fffdfa; --paper-2:#f6f3ee; --rule:#e2ddd4;
    --blue:#1b5fbe; --blue-dim:#eaf1fb;
    --green:#12724b; --amber:#9a5b09; --red:#a92e22; --violet:#5f3aa6;
    --grid:#ece7df;
    --mono:'IBM Plex Mono',ui-monospace,monospace;
    --sans:'Inter',system-ui,sans-serif;
    --serif:'Source Serif 4',Georgia,serif;
    --measure:90ch;
  }
  *{box-sizing:border-box}
  section.post{max-width:940px} 
  .wrap{max-width:1140px;margin:0 auto;padding:0 26px}
  .hero{padding:44px 0 32px}
  .eyebrow{
    font-family:var(--mono);font-size:11.5px;letter-spacing:.12em;text-transform:uppercase;
    color:var(--blue);font-weight:500;margin-bottom:18px;
  }
  h1{
    font-family:var(--sans);font-weight:700;letter-spacing:-.03em;
    font-size:clamp(31px,5.4vw,55px);line-height:1.05;margin:0 0 20px;max-width:26ch;color:var(--ink);
  }
  .dek{font-size:clamp(18px,2.2vw,22px);line-height:1.55;color:var(--ink-2);max-width:60ch;margin:0 0 22px}
  .dek strong{color:var(--ink);font-weight:600}
  .byline{
    font-family:var(--sans);font-size:13px;color:var(--ink-3);
    display:flex;flex-wrap:wrap;gap:6px 16px;padding-top:16px;border-top:1px solid var(--rule);
  }

  .metrics{display:grid;grid-template-columns:repeat(auto-fit,minmax(150px,1fr));border-top:1px solid var(--rule);background:var(--paper-2)}
  .metric{padding:18px 20px;border-right:1px solid var(--rule);border-bottom:1px solid var(--rule)}
  .metric .n{font-family:var(--mono);font-size:clamp(23px,3.2vw,31px);font-weight:500;letter-spacing:-.02em;line-height:1;color:var(--blue)}
  .metric .l{font-family:var(--sans);font-size:11px;color:var(--ink-3);text-transform:uppercase;letter-spacing:.08em;margin-top:8px;line-height:1.4}
  .metric.warn .n{color:var(--amber)}
  .metric.plain .n{color:var(--ink)}

  main{max-width:1140px;margin:0 auto;padding:0 26px 90px}
  section{padding-top:56px}
  h2{
    font-family:var(--sans);font-size:clamp(23px,3vw,32px);font-weight:700;letter-spacing:-.025em;
    line-height:1.15;margin:0 0 18px;color:var(--ink);
  }
  h3{font-family:var(--sans);font-size:19px;font-weight:600;margin:34px 0 10px;letter-spacing:-.015em}
  h4{font-family:var(--sans);font-size:15px;font-weight:600;margin:24px 0 6px;color:var(--ink-2)}
  p{margin:0 0 18px;max-width:var(--measure)}
  ul,ol{max-width:var(--measure);padding-left:24px;margin:0 0 18px}
  li{margin-bottom:9px}
  a{color:var(--blue);text-decoration:none;border-bottom:1px solid rgba(27,95,190,.32)}
  a:hover{border-bottom-color:var(--blue)}
  code{font-family:var(--mono);font-size:.85em;background:var(--paper-2);padding:1.5px 5px;border-radius:3px;border:1px solid var(--rule)}
  strong{font-weight:600}
  .lede{font-size:20px;color:var(--ink-2);max-width:90ch;font-family:var(--serif)}

  .tw{overflow-x:auto;margin:22px 0;border:1px solid var(--rule);border-radius:5px;background:var(--paper);-webkit-overflow-scrolling:touch}
  table{border-collapse:collapse;width:100%;font-family:var(--sans);font-size:13.5px;min-width:600px}
  th,td{text-align:left;padding:10px 13px;border-bottom:1px solid var(--grid);vertical-align:top}
  thead th{
    background:var(--paper-2);font-size:10.5px;text-transform:uppercase;letter-spacing:.07em;
    color:var(--ink-3);font-weight:600;border-bottom:1px solid var(--rule);white-space:nowrap;position:sticky;top:0;
  }
  tbody tr:last-child td{border-bottom:0}
  tbody tr:hover{background:var(--blue-dim)}
  td.n,th.n{text-align:right;font-family:var(--mono);font-variant-numeric:tabular-nums;white-space:nowrap}
  td.st{font-family:var(--mono);font-size:11.5px;color:var(--ink-2);letter-spacing:.02em}
  td.web{font-size:12.5px;word-break:break-word;min-width:170px}
  .note{font-size:11.5px;color:var(--ink-3);margin-top:3px;line-height:1.45;font-weight:400}
  .hint{color:var(--ink-3);font-size:11px;font-family:var(--sans)}
  .tagx{
    display:inline-block;font-family:var(--mono);font-size:9.5px;text-transform:uppercase;letter-spacing:.07em;
    padding:1px 5px;border-radius:3px;vertical-align:middle;margin-left:5px;font-weight:500;
  }
  .tagx.approx{background:#efe8fb;color:var(--violet)}
  .tagx.planned{background:#fbf1de;color:var(--amber)}

  .btn-map{
    display:inline-block;font-family:var(--sans);font-weight:600;font-size:15px;
    color:#fff;background:var(--blue);padding:12px 24px;border-radius:6px;
    text-decoration:none;border-bottom:none;margin:4px 0 8px;
    box-shadow:0 1px 2px rgba(21,24,29,.12);transition:background .15s ease;
  }
  .btn-map:hover{background:#154c99;border-bottom:none}

  .call{border-left:3px solid var(--blue);background:var(--blue-dim);padding:16px 19px;margin:24px 0;border-radius:0 5px 5px 0;max-width:var(--measure)}
  .call.warn{border-left-color:var(--amber);background:#fdf6e9}
  .call.stop{border-left-color:var(--red);background:#fdf0ee}
  .call.ok{border-left-color:var(--green);background:#ecf6f1}
  .call.mine{border-left-color:var(--violet);background:#f4f0fc}
  .call p:last-child{margin-bottom:0}
  .call .h{font-family:var(--mono);font-size:10.5px;text-transform:uppercase;letter-spacing:.1em;font-weight:500;display:block;margin-bottom:8px;color:var(--blue)}
  .call.warn .h{color:var(--amber)} .call.stop .h{color:var(--red)}
  .call.ok .h{color:var(--green)} .call.mine .h{color:var(--violet)}

  pre{background:#15181d;color:#d9dee6;padding:15px 17px;border-radius:5px;overflow-x:auto;font-family:var(--mono);font-size:12.5px;line-height:1.6;margin:20px 0}
  pre code{background:none;border:0;padding:0;color:inherit;font-size:inherit}
  .c-dim{color:#7d8895} .c-g{color:#66c98f} .c-a{color:#dfae57} .c-r{color:#ef8b82}

  /* bar charts — .fill MUST be display:block; a sized <span> computes to
     display:inline and would render at 0x0 (this exact bug shipped once) */
  .bars{margin:22px 0;max-width:600px}
  .bar{display:grid;grid-template-columns:74px 1fr 74px;gap:11px;align-items:center;margin-bottom:6px;font-family:var(--sans);font-size:13px}
  .bar .lbl{text-align:right;color:var(--ink-2);font-weight:500}
  .bar .track{background:var(--grid);height:18px;border-radius:2px;overflow:hidden;display:flex}
  .bar .fill{display:block;flex:0 0 auto;height:100%;background:var(--blue)}
  .bar .fill.osm{background:var(--ink-3)}
  .bar .fill.approx{background:var(--violet)}
  .bar .val{font-family:var(--mono);font-size:11.5px;color:var(--ink-3);font-variant-numeric:tabular-nums}
  .caption{font-size:13px;color:var(--ink-3);max-width:58ch;font-family:var(--sans);margin-top:-6px}

  .toc{background:var(--paper-2);border:1px solid var(--rule);border-radius:6px;padding:18px 22px;margin:28px 0;max-width:var(--measure)}
  .toc ol{margin:0;padding-left:20px;font-family:var(--sans);font-size:14px}
  .toc li{margin-bottom:5px}

  footer{border-top:1px solid var(--rule);background:var(--paper-2);padding:34px 26px;font-size:14px;color:var(--ink-3)}
  .foot{max-width:1140px;margin:0 auto}
  .foot p{max-width:none}

  @media (max-width:640px){
    body{font-size:17px}
    .wrap,main{padding-left:18px;padding-right:18px}
    footer{padding:26px 18px}
    section{padding-top:42px}
    .bar{grid-template-columns:58px 1fr 58px;gap:8px;font-size:12px}
  }
</style>
<style>
.ha-img-placeholder{display:flex;align-items:center;justify-content:center;flex-direction:column;gap:6px;background:#f4f4f5;border:1px dashed #d4d4d8;border-radius:8px;color:#71717a;font-size:12px;font-family:system-ui,sans-serif;min-height:80px;padding:16px;box-sizing:border-box;animation:ha-img-pulse 1.5s ease-in-out infinite}
.ha-img-placeholder.ha-failed{animation:none;opacity:.7}
@keyframes ha-img-pulse{0%,100%{opacity:1}50%{opacity:.5}}
@media(prefers-color-scheme:dark){.ha-img-placeholder{background:#27272a;border-color:#3f3f46;color:#a1a1aa}}
</style>


<div class="wrap hero">
  <h2>What I learned vibe coding an EV charging map out of Australian open data</h2>
  <p class="dek">
    Six openly-licensed government and community datasets. <strong>3,157 charging
    sites</strong>, <strong>9,216 plugs</strong>, and a long list of things that
    went wrong. Some of them were the publishers' fault. Several were mine.
  </p>
</div>


<div class="metrics">
  <div class="metric"><div class="n">3,102</div><div class="l">Mappable sites</div></div>
  <div class="metric"><div class="n">9,216</div><div class="l">Plugs (a floor, see below)</div></div>
  <div class="metric plain"><div class="n">6</div><div class="l">Open sources, no API keys</div></div>
  <div class="metric plain"><div class="n">38</div><div class="l">Operators with 5+ sites</div></div>
  <div class="metric warn"><div class="n">214</div><div class="l">Distinct operator strings</div></div>
</div>

<main>

<section id="intro">
  <h2>The short version</h2>
  <p class="lede">
    Australia has no national EV charger dataset. What it has is one community database, four state
    government publications, one territory's policy page, and a lot of inconsistency between them.
    Stitched together carefully, that gets you about four fifths of the country's public charging.
  </p>
  <p>
    I wanted to know whether you could build a genuinely useful national charging map without paying
    for commercial data. The answer is <em>mostly yes</em>. The interesting part isn't the total,
    though: it's everything that goes wrong on the way there.
  </p>
  <p>
    This post covers three things: what the combined data actually says about sites and plugs, who
    operates them (with a table of every operator I could identify and a verified link to each), and
    every problem I hit. I've been deliberate about including the mistakes that were mine, because
    they were more instructive than the publishers'.
  </p>
  <p><a class="btn-map" href="https://map.electricvehicle.life" target="_blank" rel="noopener noreferrer">Explore the map →</a></p>

  <div class="toc">
    <ol>
      <li><a href="#overview">Sites and plugs: what the data says</a></li>
      <li><a href="#operators">Every operator I could identify</a></li>
      <li><a href="#sources">Where the data comes from</a></li>
      <li><a href="#issues">Everything that went wrong</a></li>
      <li><a href="#advice">What I'd tell you before you start</a></li>
    </ol>
  </div>
</section>

<section id="overview">
  <h2>Sites and plugs: what the data says</h2>
  <p>
    After reconciling 3,806 source records into
    3,157 distinct sites (merging 649 duplicates
    along the way), here is the shape of Australian public charging as open data sees it.
  </p>

  <h3>Sites by state and territory</h3>
  <div class="bars">
<div class="bar"><span class="lbl">NSW</span><span class="track"><span class="fill" style="width:100.0%"></span></span><span class="val">1,906</span></div>
<div class="bar"><span class="lbl">VIC</span><span class="track"><span class="fill" style="width:31.4%"></span></span><span class="val">598</span></div>
<div class="bar"><span class="lbl">QLD</span><span class="track"><span class="fill" style="width:12.2%"></span></span><span class="val">233</span></div>
<div class="bar"><span class="lbl">TAS</span><span class="track"><span class="fill" style="width:4.2%"></span><span class="fill approx" style="width:2.9%"></span></span><span class="val">81+55</span></div>
<div class="bar"><span class="lbl">WA</span><span class="track"><span class="fill" style="width:5.4%"></span></span><span class="val">103</span></div>
<div class="bar"><span class="lbl">SA</span><span class="track"><span class="fill" style="width:5.4%"></span></span><span class="val">102</span></div>
<div class="bar"><span class="lbl">ACT</span><span class="track"><span class="fill" style="width:3.0%"></span></span><span class="val">58</span></div>
<div class="bar"><span class="lbl">NT</span><span class="track"><span class="fill" style="width:1.1%"></span></span><span class="val">21</span></div>
  </div>
  <p class="caption">
    Tasmania's bar is split: 81 sites with real coordinates, plus
    55 known only to the town (purple). NSW dominates partly through genuine density and
    partly because NSW simply publishes more than anyone else.
  </p>

  <h3>Sites by charging speed</h3>
  <div class="bars">
<div class="bar"><span class="lbl">250 kW+</span><span class="track"><span class="fill" style="width:10.1%"></span></span><span class="val">146</span></div>
<div class="bar"><span class="lbl">150–249</span><span class="track"><span class="fill" style="width:10.6%"></span></span><span class="val">153</span></div>
<div class="bar"><span class="lbl">50–149</span><span class="track"><span class="fill" style="width:30.1%"></span></span><span class="val">435</span></div>
<div class="bar"><span class="lbl">25–49</span><span class="track"><span class="fill" style="width:5.3%"></span></span><span class="val">76</span></div>
<div class="bar"><span class="lbl">7–24</span><span class="track"><span class="fill" style="width:54.4%"></span></span><span class="val">787</span></div>
<div class="bar"><span class="lbl"><7 kW</span><span class="track"><span class="fill" style="width:7.8%"></span></span><span class="val">113</span></div>
<div class="bar"><span class="lbl">not stated</span><span class="track"><span class="fill osm" style="width:100.0%"></span></span><span class="val">1,447</span></div>
  </div>
  <p class="caption">
    The grey bar is the problem: 1,447 sites
    (46%) have no usable power rating in
    any source. Only 54% of sites state their kilowatts.
  </p>

  <div class="call warn">
    <span class="h">Read the plug count as a floor, not a total</span>
    <p>
      9,216 plugs is the sum of every plug count actually published,
      and only <strong>83% of sites publish one</strong>.
      The true figure is higher. For comparison, the commercial tracker Carloop put Australia at
      roughly 3,774 sites and 10,350 bays in August 2026, which makes this open-data build about
      <strong>82% of sites and 89% of bays</strong>, but those two numbers are counting slightly
      different things, and anyone who quotes a single national plug total with confidence is
      overselling it.
    </p>
  </div>

  <h3>How complete is each field?</h3>
  <p>
    Coverage is not the same as completeness. A site can be in the data and still tell you almost
    nothing useful:
  </p>
  <div class="">
    <table>
      <thead><tr><th>Field</th><th class="n">Sites with a value</th><th>What that means in practice</th></tr></thead>
      <tbody>
        <tr><td>Connector type</td><td class="n">87%</td><td>Usually the best-populated field</td></tr>
        <tr><td>Site name</td><td class="n">64%</td><td>The rest fall back to operator + suburb</td></tr>
        <tr><td>Power rating (kW)</td><td class="n">54%</td><td>You cannot filter reliably by charging speed</td></tr>
        <tr><td>Confirmed by 2+ sources</td><td class="n">12%</td><td>Each dataset covers a different state, so overlap is rare</td></tr>
        <tr><td>Pricing</td><td class="n">0%</td><td>Not published by anyone, anywhere</td></tr>
        <tr><td>Live availability</td><td class="n">0%</td><td>No open source exists; see the last section</td></tr>
      </tbody>
    </table>
  </div>
</section>

<section id="operators">
  <h2>Every operator I could identify</h2>
  <p class="lede">
    This is the table I most wanted when I started and could not find anywhere.
  </p>
  <p>
    38 operators run five or more sites, accounting for 2,928 of 3,157 sites (93%).
  </p>

  <div class="call">
    <span class="h">How to read this table</span>
    <p>
      <strong>Sites</strong> is reconciled physical locations, not charger units.
      <strong>Plugs</strong> is the sum of published plug counts, so where a operator's sites don't
      all publish one the figure is a floor: the percentage tells you how much of it is covered.
      <strong>Max kW</strong> is the highest per-plug rating stated anywhere in that operator's
      records. Two of these names are truncated <em>in the source data itself</em>, which I've left
      as-is and flagged rather than silently "fixing".
    </p>
  </div>

  <div class="">
    <table>
      <thead>
        <tr>
          <th>Operator</th><th class="n">Sites</th><th class="n">Plugs</th><th class="n">Max kW</th>
          <th>Current mix</th><th>States</th><th>Website</th>
        </tr>
      </thead>
      <tbody>
<tr>
      <td><strong>Chargefox</strong> <span class="tagx planned">3 planned</span><div class="note">Owned by NRMA, RACV, RACQ, RAA, RAC and RACT</div></td>
      <td class="n">449</td>
      <td class="n">1,728 <span class="hint">from 81% of sites</span></td>
      <td class="n">350</td>
      <td>194 DC <br> 270 AC</td>
      <td class="st">ACT NSW NT QLD SA TAS VIC WA</td>
      <td class="web"><a href="https://www.chargefox.com/" target="_blank" rel="noopener noreferrer">www.chargefox.com</a></td>
    </tr>
<tr>
      <td><strong>Tesla</strong> <span class="tagx planned">24 planned</span><div class="note">Supercharger + Destination</div></td>
      <td class="n">416</td>
      <td class="n">1,694 <span class="hint">from 93% of sites</span></td>
      <td class="n">350</td>
      <td>174 DC <br> 211 AC</td>
      <td class="st">ACT NSW QLD SA TAS VIC WA</td>
      <td class="web"><a href="https://www.tesla.com/en_AU/findus/list/superchargers/Australia" target="_blank" rel="noopener noreferrer">www.tesla.com/en_AU/findus/list/superchargers/Australia</a></td>
    </tr>
<tr>
      <td><strong>Exploren</strong><div class="note">OCPP platform; many host-operated sites</div></td>
      <td class="n">325</td>
      <td class="n">914 <span class="hint">from 98% of sites</span></td>
      <td class="n">180</td>
      <td>52 DC <br> 272 AC</td>
      <td class="st">ACT NSW TAS VIC WA</td>
      <td class="web"><a href="https://exploren.com.au/" target="_blank" rel="noopener noreferrer">exploren.com.au</a></td>
    </tr>
<tr>
      <td><strong>Evie Networks</strong> <span class="tagx planned">8 planned</span><div class="note">OSM still lists the old goevie.com.au</div></td>
      <td class="n">290</td>
      <td class="n">1,095 <span class="hint">from 79% of sites</span></td>
      <td class="n">400</td>
      <td>264 DC <br> 5 AC</td>
      <td class="st">ACT NSW NT QLD SA TAS VIC WA</td>
      <td class="web"><a href="https://evie.com.au/" target="_blank" rel="noopener noreferrer">evie.com.au</a></td>
    </tr>
<tr>
      <td><strong>(unattributed)</strong><div class="note">No operator published by any source</div></td>
      <td class="n">221</td>
      <td class="n">253 <span class="hint">from 38% of sites</span></td>
      <td class="n">400</td>
      <td>35 DC <br> 51 AC</td>
      <td class="st">ACT NSW NT QLD SA TAS VIC WA</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>Non-networked</strong> <span class="tagx planned">4 planned</span><div class="note">A TfNSW placeholder, not an operator</div></td>
      <td class="n">218</td>
      <td class="n">528</td>
      <td class="n">22</td>
      <td>214 AC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>NRMA</strong> <span class="tagx planned">16 planned</span></td>
      <td class="n">132</td>
      <td class="n">735 <span class="hint">from 91% of sites</span></td>
      <td class="n">350</td>
      <td>123 DC <br> 14 AC</td>
      <td class="st">NSW NT QLD SA TAS VIC WA</td>
      <td class="web"><a href="https://www.mynrma.com.au/electric-vehicles/charging" target="_blank" rel="noopener noreferrer">www.mynrma.com.au/electric-vehicles/charging</a></td>
    </tr>
<tr>
      <td><strong>PLUS ES</strong> <span class="tagx planned">2 planned</span><div class="note">Ausgrid subsidiary; kerbside AC</div></td>
      <td class="n">106</td>
      <td class="n">114</td>
      <td class="n">350</td>
      <td>2 DC <br> 104 AC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://www.pluses.com.au/" target="_blank" rel="noopener noreferrer">www.pluses.com.au</a></td>
    </tr>
<tr>
      <td><strong>EVX</strong><div class="note">Pole-mounted kerbside</div></td>
      <td class="n">103</td>
      <td class="n">221 <span class="hint">from 99% of sites</span></td>
      <td class="n">22</td>
      <td>103 AC</td>
      <td class="st">NSW VIC</td>
      <td class="web"><a href="https://evx.tech/" target="_blank" rel="noopener noreferrer">evx.tech</a></td>
    </tr>
<tr>
      <td><strong>BP Pulse</strong> <span class="tagx planned">29 planned</span></td>
      <td class="n">86</td>
      <td class="n">303 <span class="hint">from 87% of sites</span></td>
      <td class="n">350</td>
      <td>81 DC</td>
      <td class="st">ACT NSW QLD TAS VIC</td>
      <td class="web"><a href="https://www.bppulse.com/en-au" target="_blank" rel="noopener noreferrer">www.bppulse.com/en-au</a></td>
    </tr>
<tr>
      <td><strong>Ampol</strong><div class="note">AmpCharge</div></td>
      <td class="n">81</td>
      <td class="n">323 <span class="hint">from 85% of sites</span></td>
      <td class="n">400</td>
      <td>76 DC</td>
      <td class="st">ACT NSW QLD VIC</td>
      <td class="web"><a href="https://www.ampol.com.au/ampcharge" target="_blank" rel="noopener noreferrer">www.ampol.com.au/ampcharge</a></td>
    </tr>
<tr>
      <td><strong>JOLT</strong></td>
      <td class="n">72</td>
      <td class="n">91 <span class="hint">from 69% of sites</span></td>
      <td class="n">50</td>
      <td>56 DC <br> 4 AC</td>
      <td class="st">NSW QLD SA VIC</td>
      <td class="web"><a href="https://joltcharge.com/au/" target="_blank" rel="noopener noreferrer">joltcharge.com/au</a></td>
    </tr>
<tr>
      <td><strong>Yurika</strong><div class="note">Energy Queensland</div></td>
      <td class="n">50</td>
      <td class="n">60 <span class="hint">from 58% of sites</span></td>
      <td class="n">75</td>
      <td>47 DC <br> 41 AC</td>
      <td class="st">NT QLD</td>
      <td class="web"><a href="https://www.yurika.com.au/" target="_blank" rel="noopener noreferrer">www.yurika.com.au</a></td>
    </tr>
<tr>
      <td><strong>EVUp</strong></td>
      <td class="n">39</td>
      <td class="n">82 <span class="hint">from 97% of sites</span></td>
      <td class="n">100</td>
      <td>1 DC <br> 35 AC</td>
      <td class="st">NSW QLD VIC</td>
      <td class="web"><a href="https://www.evup.com.au/" target="_blank" rel="noopener noreferrer">www.evup.com.au</a></td>
    </tr>
<tr>
      <td><strong>Everty</strong></td>
      <td class="n">34</td>
      <td class="n">81</td>
      <td class="n">60</td>
      <td>4 DC <br> 30 AC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://everty.com.au/" target="_blank" rel="noopener noreferrer">everty.com.au</a></td>
    </tr>
<tr>
      <td><strong>Central Victorian Greenhouse Alliance</strong><div class="note">Council alliance, not a network</div></td>
      <td class="n">34</td>
      <td class="n">31 <span class="hint">from 44% of sites</span></td>
      <td class="n">62</td>
      <td>30 DC</td>
      <td class="st">VIC</td>
      <td class="web"><a href="https://www.cvga.org.au/" target="_blank" rel="noopener noreferrer">www.cvga.org.au</a></td>
    </tr>
<tr>
      <td><strong>Electric Highway Tasmania</strong> <span class="tagx approx">town-level</span><div class="note">Own domain now redirects to NRMA</div></td>
      <td class="n">30</td>
      <td class="n">19 <span class="hint">from 37% of sites</span></td>
      <td class="n">350</td>
      <td>25 DC <br> 12 AC</td>
      <td class="st">TAS</td>
      <td class="web"><a href="https://www.mynrma.com.au/electric-vehicles/charging/electric-highway-tasmania" target="_blank" rel="noopener noreferrer">www.mynrma.com.au/electric-vehicles/charging/electric-highway-tasmania</a></td>
    </tr>
<tr>
      <td><strong>Smart Charge</strong></td>
      <td class="n">23</td>
      <td class="n">57</td>
      <td class="n"><span class="hint">—</span></td>
      <td>23 AC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://smartcharge.com.au/" target="_blank" rel="noopener noreferrer">smartcharge.com.au</a></td>
    </tr>
<tr>
      <td><strong>EVE Australia</strong><div class="note">No site resolved; may be EVSE Australia</div></td>
      <td class="n">21</td>
      <td class="n">52</td>
      <td class="n">60</td>
      <td>2 DC <br> 19 AC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>ChargePoint</strong><div class="note">Global hardware/network</div></td>
      <td class="n">20</td>
      <td class="n">30 <span class="hint">from 60% of sites</span></td>
      <td class="n">22</td>
      <td>10 AC</td>
      <td class="st">NSW QLD SA VIC WA</td>
      <td class="web"><a href="https://www.chargepoint.com/" target="_blank" rel="noopener noreferrer">www.chargepoint.com</a></td>
    </tr>
<tr>
      <td><strong>EVSE</strong><div class="note">Possibly the same firm as &quot;EVE Australia&quot;</div></td>
      <td class="n">20</td>
      <td class="n">52</td>
      <td class="n">22</td>
      <td>20 AC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://evse.com.au/" target="_blank" rel="noopener noreferrer">evse.com.au</a></td>
    </tr>
<tr>
      <td><strong>Porsche Smart Mobility</strong><div class="note">Destination charging at dealers</div></td>
      <td class="n">19</td>
      <td class="n">39</td>
      <td class="n">22</td>
      <td>19 AC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>Fast Cities Australia</strong> <span class="tagx planned">17 planned</span>
      <div class="note">Name truncated in the TfNSW export; no site found</div></td>
      <td class="n">17</td>
      <td class="n">80</td>
      <td class="n">350</td>
      <td>17 DC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>EVNet</strong><div class="note">Rebranded: evnet.com.au now redirects here</div></td>
      <td class="n">15</td>
      <td class="n">35</td>
      <td class="n"><span class="hint">—</span></td>
      <td>15 AC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://chargengo.au/" target="_blank" rel="noopener noreferrer">chargengo.au</a></td>
    </tr>
<tr>
      <td><strong>Noodoe</strong></td>
      <td class="n">13</td>
      <td class="n">28</td>
      <td class="n"><span class="hint">—</span></td>
      <td>12 AC</td>
      <td class="st">NSW QLD</td>
      <td class="web"><a href="https://www.noodoe.com/" target="_blank" rel="noopener noreferrer">www.noodoe.com</a></td>
    </tr>
<tr>
      <td><strong>ChargeHub</strong><div class="note">No site resolved</div></td>
      <td class="n">12</td>
      <td class="n">36</td>
      <td class="n">25</td>
      <td>1 DC <br> 10 AC</td>
      <td class="st">NSW WA</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>EVlink</strong><div class="note">No site resolved</div></td>
      <td class="n">10</td>
      <td class="n">12 <span class="hint">from 70% of sites</span></td>
      <td class="n"><span class="hint">—</span></td>
      <td>1 AC</td>
      <td class="st">NSW QLD VIC WA</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>Elanga</strong></td>
      <td class="n">9</td>
      <td class="n">34</td>
      <td class="n">50</td>
      <td>1 DC <br> 8 AC</td>
      <td class="st">NSW QLD</td>
      <td class="web"><a href="https://elanga.com.au/" target="_blank" rel="noopener noreferrer">elanga.com.au</a></td>
    </tr>
<tr>
      <td><strong>Viva Energy Australia</strong> <span class="tagx planned">6 planned</span><div class="note">Name truncated in the TfNSW export</div></td>
      <td class="n">7</td>
      <td class="n">28</td>
      <td class="n">350</td>
      <td>7 DC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://www.vivaenergy.com.au/" target="_blank" rel="noopener noreferrer">www.vivaenergy.com.au</a></td>
    </tr>
<tr>
      <td><strong>Charge Hub</strong><div class="note">Possibly the same as &quot;ChargeHub&quot;, unresolved</div></td>
      <td class="n">7</td>
      <td class="n">19</td>
      <td class="n"><span class="hint">—</span></td>
      <td>7 AC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>Engie</strong><div class="note">Site returns an authorisation error from Australia</div></td>
      <td class="n">7</td>
      <td class="n">12 <span class="hint">from 86% of sites</span></td>
      <td class="n">150</td>
      <td>7 DC</td>
      <td class="st">NSW VIC</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>Saascharge</strong><div class="note">No site resolved</div></td>
      <td class="n">7</td>
      <td class="n">17</td>
      <td class="n">22</td>
      <td>7 AC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>INTELLIHUB AUSTRALIA Pty Ltd</strong></td>
      <td class="n">7</td>
      <td class="n">11</td>
      <td class="n">22</td>
      <td>7 AC</td>
      <td class="st">VIC</td>
      <td class="web"><a href="https://www.intellihub.com.au/" target="_blank" rel="noopener noreferrer">www.intellihub.com.au</a></td>
    </tr>
<tr>
      <td><strong>Electrona Pty Ltd</strong> <span class="tagx approx">town-level</span></td>
      <td class="n">7</td>
      <td class="n">3 <span class="hint">from 29% of sites</span></td>
      <td class="n">22</td>
      <td>1 DC <br> 1 AC</td>
      <td class="st">TAS</td>
      <td class="web"><a href="https://electrona.com.au/" target="_blank" rel="noopener noreferrer">electrona.com.au</a></td>
    </tr>
<tr>
      <td><strong>CasaCharge</strong></td>
      <td class="n">6</td>
      <td class="n">13</td>
      <td class="n"><span class="hint">—</span></td>
      <td>1 DC <br> 5 AC</td>
      <td class="st">NSW WA</td>
      <td class="web"><a href="https://casacharge.com.au/" target="_blank" rel="noopener noreferrer">casacharge.com.au</a></td>
    </tr>
<tr>
      <td><strong>Brisbane Airport Corporation</strong><div class="note">Airport, not a network</div></td>
      <td class="n">5</td>
      <td class="n">28</td>
      <td class="n"><span class="hint">—</span></td>
      <td><span class="hint">not published</span></td>
      <td class="st">QLD</td>
      <td class="web"><a href="https://www.bne.com.au/" target="_blank" rel="noopener noreferrer">www.bne.com.au</a></td>
    </tr>
<tr>
      <td><strong>360 EV Charge</strong><div class="note">No site resolved</div></td>
      <td class="n">5</td>
      <td class="n">15</td>
      <td class="n"><span class="hint">—</span></td>
      <td>5 AC</td>
      <td class="st">NSW</td>
      <td class="web"><span class="hint">—</span></td>
    </tr>
<tr>
      <td><strong>Charge Post</strong></td>
      <td class="n">5</td>
      <td class="n">40</td>
      <td class="n">22</td>
      <td>5 AC</td>
      <td class="st">NSW</td>
      <td class="web"><a href="https://www.chargepost.com.au/" target="_blank" rel="noopener noreferrer">www.chargepost.com.au</a></td>
    </tr>
      </tbody>
    </table>
  </div>

  <div class="call warn">
    <span class="h">And then there is the long tail</span>
    <p>
      Beyond those 38 operators sit <strong>176 more</strong>
      with fewer than five sites each (229 sites and 303
      plugs between them). <strong>143 of them appear exactly once.</strong>
      They are pubs, councils, wineries, golf clubs and car dealers who installed a charger and got
      named as its operator. That's 214 distinct operator strings in
      total, and it is the single clearest sign that "operator" in this data means several different
      things at once: a charging network, a site host, a hardware platform, or a placeholder.
    </p>
  </div>
</section>

<section id="sources">
  <h2>Where the data comes from</h2>
  <p>
    Six sources, all openly licensed, none requiring an API key. Note how lopsided the record counts
    are, and that three of the six are limited to government-funded sites rather than everything in
    their state.
  </p>
  <div class="">
    <table>
      <thead><tr><th>Source</th><th>Covers</th><th class="n">Records</th><th>Licence</th><th>Refresh</th></tr></thead>
      <tbody>
<tr>
      <td><strong>OpenStreetMap (Overpass API)</strong><div class="note">Full published extent</div></td>
      <td>AU (national)</td>
      <td class="n">1,590</td>
      <td>ODbL 1.0</td>
      <td>daily</td>
    </tr>
<tr>
      <td><strong>Transport for NSW</strong><div class="note">Full published extent</div></td>
      <td>NSW</td>
      <td class="n">1,958</td>
      <td>CC-BY 3.0 AU</td>
      <td>weekly</td>
    </tr>
<tr>
      <td><strong>Victoria DEECA</strong><div class="note">Government-funded chargers only: not a complete Victorian inventory.</div></td>
      <td>VIC</td>
      <td class="n">152</td>
      <td>CC-BY 4.0</td>
      <td>weekly</td>
    </tr>
<tr>
      <td><strong>QLD Transport and Main Roads</strong><div class="note">Queensland Electric Super Highway sites only: not a complete QLD inventory.</div></td>
      <td>QLD</td>
      <td class="n">17</td>
      <td>CC-BY 4.0</td>
      <td>weekly</td>
    </tr>
<tr>
      <td><strong>ACT Government</strong><div class="note">ACT Government-funded chargers only (131 bays). The page states 300+ public bays exist territory-wide, so this is roughly 44% of ACT public charging.</div></td>
      <td>ACT</td>
      <td class="n">34</td>
      <td>CC-BY 4.0</td>
      <td>weekly</td>
    </tr>
<tr>
      <td><strong>Tasmania NRE</strong><div class="note">ChargeSmart grant recipients only, located to TOWN level with no street address. Excluded from the default map view; not a charger location.</div></td>
      <td>TAS</td>
      <td class="n">55</td>
      <td>CC-BY 4.0</td>
      <td>monthly</td>
    </tr>
      </tbody>
    </table>
  </div>

  <div class="call stop">
    <span class="h">The licensing trap worth knowing before you build</span>
    <p>
      Mixing OpenStreetMap into your own stored database makes that database an ODbL
      <strong>"Derivative Database"</strong>, not merely a "Produced Work", so share-alike attaches
      to the whole thing, and you must offer it back under ODbL on request. Combining CC-BY
      government data with OSM does <em>not</em> give you a CC-BY database. If your plan depends on
      keeping the result proprietary, that plan breaks, and you will discover it after building
      rather than before.
    </p>
  </div>

  <div class="call">
    <span class="h">Three of eight jurisdictions publish nothing usable</span>
    <p>
      South Australia's page shows only an indicative design map; Western Australia's "charger map"
      is a JPEG inside a PDF <em>and</em> its terms are non-commercial only; the Northern Territory
      points you at PlugShare. Coverage in those three comes entirely from OpenStreetMap volunteers.
    </p>
  </div>
</section>

<section id="issues">
  <h2>Everything that went wrong</h2>
  <p class="lede">
    This is the useful part. Not one of these was visible from a dataset's description page: every
    one turned up only by running the data.
  </p>

  <h3>1. The open-data portal is not where the open data is</h3>
  <p>
    I originally recorded the ACT as publishing nothing, because its open-data portal returns only
    federated US Department of Energy records. The ACT in fact publishes its full funded-charger
    list as <strong>HTML tables on a policy page</strong>. Re-checking the other jurisdictions with
    that lesson in hand found Tasmania doing the same thing.
  </p>
  <div class="call stop">
    <span class="h">The generalisable version</span>
    <p>
      Any data survey that only queries portal APIs will under-report. Policy, programme and grant
      pages need checking too. Two of eight Australian jurisdictions publish charger lists that no
      portal search will ever surface.
    </p>
  </div>

  <h3>2. A field that means the opposite of what it looks like</h3>
  <p>
    Victoria publishes <code>estimated_project_completion</code>. The obvious reading (populated
    means "not yet built") is wrong. 96 of 152 rows carry a value and most are <em>past</em> dates
    describing finished projects. Taking it at face value mislabelled 96 live sites as unbuilt.
    Parsing the date and comparing it to today gives the real answer: <strong>11</strong>. Australian
    dates are day-first, so <code>Date.parse()</code> can't be used either.
  </p>

  <h3>3. Truncated names, in the publisher's own export</h3>
  <p>
    The TfNSW dataset contains <code>"Fast Cities A"</code> (17 rows) and <code>"Viva Energy A"</code>
    (7 rows): names cut off mid-word. Inconsistently, too: one row has the full
    <code>"Viva Energy Australia"</code>, which splits one company across two operator values. A
    third appears as <code>"PLUS ES Manag"</code>. I left them as published and flagged them, because
    silently "correcting" a publisher's data is how you end up with a dataset nobody can reconcile
    against the source.
  </p>

  <h3>4. HTTP 200 is not the same as good data</h3>
  <p>
    Two separate incidents, both silent:
  </p>
  <ul>
    <li>Queensland's CSV returns <strong>HTTP 403 with an HTML error page</strong> to any
      non-browser client. A lenient parser reads that as a zero-row CSV. The same file also has
      newlines inside quoted fields, so it holds 17 records where <code>wc -l</code> claims 34.</li>
    <li>When the primary OpenStreetMap endpoint timed out, the fallback mirror returned a
      complete-looking <strong>1,493 elements against the primary's 1,590</strong>. Nothing
      truncated, no warning: the mirror's database was simply <strong>43 days stale</strong>. A
      silent 6% coverage loss, small enough to slip under any record-count threshold.</li>
  </ul>
  <div class="call ok">
    <span class="h">What actually fixes this</span>
    <p>
      Validate a response <em>before</em> it replaces your cached copy, and where a source states its
      own data age, <strong>check it</strong>. Overpass reports <code>timestamp_osm_base</code>;
      refusing anything older than a week turns an invisible degradation into a loud failure. Your
      own slightly-old cache beats someone else's much-older data.
    </p>
  </div>

  <h3>5. Publishers state totals: use them</h3>
  <p>
    The single most useful trick I found. Both the ACT and Tasmanian pages state their own
    figures: "74 public EV chargers with 131 charging bays", and a <code>Total</code> row of
    <code>$567,000</code>, so the parse can reconcile against them and refuse the page if it
    doesn't add up. That converts a fragile HTML scrape into one that fails loudly when the page is
    restructured, instead of quietly returning fewer rows.
  </p>
  <pre><code><span class="c-dim">// Tasmania: three tables, three stated grant totals, all reconciling</span>
Chargesmart 3 2025    parsed $567000  stated $567000  <span class="c-g">OK</span>
Fast charging         parsed $710500  stated $710500  <span class="c-g">OK</span>
Destination charging  parsed $62500   stated $62500   <span class="c-g">OK</span></code></pre>

  <h3>6. Some coordinates are not locations</h3>
  <p>
    Tasmania publishes a town name and nothing finer. Geocoding "Miena" gives you the centre of
    Miena, which for a charger is kilometres out. So precision became a graded property rather than
    a boolean: <code>surveyed</code> (~10 m), <code>geocoded_address</code> (~100 m, the ACT), and
    <code>geocoded_locality</code> (~5 km, Tasmania). Town-level records are excluded from the map by
    default, never merged with anything, never returned as "nearest", and confidence-penalised,
    while staying fully searchable, because "is there a funded charger in Miena?" is a real question
    nothing else answers.
  </p>

  <h3>7. Stale and rebranded links inside the data</h3>
  <p>
    While verifying operator websites I found OpenStreetMap still carrying
    <code>goevie.com.au</code> for Evie Networks, whose site is now <code>evie.com.au</code>. EVNet's
    domain redirects to <code>chargengo.au</code>, a rebrand. Electric Highway Tasmania's domain now
    lands on an NRMA page. Any URL you inherit from crowd-sourced data needs checking before you
    put it in front of a user.
  </p>

  <h3>8. "Operator" is four different concepts</h3>
  <p>
    214 distinct values, 143 of them
    appearing once. <code>"Non-networked"</code> (218 sites) is a TfNSW placeholder, not a company.
    <code>"ChargeHub"</code> (12 sites) and <code>"Charge Hub"</code> (7) may or may not be the same
    business: I could not resolve it, so I left them separate and said so. Same for
    <code>"EVE Australia"</code> and <code>"EVSE"</code>. Alias folding fixes the obvious cases
    ("BP" and "BP Australia"; "Tesla" and "Tesla Motors") but you cannot fold what you cannot verify.
  </p>

  <h3>9. The bugs that were mine</h3>
  <p>
    Worth separating out, because they were the most instructive failures and none of them were
    the publishers' doing.
  </p>

  <div class="call mine">
    <span class="h">A 350 kW AC charger, which cannot exist</span>
    <p>
      Two PLUS ES sites showed 350 kW on an <strong>AC</strong> connector. AC charging tops out
      around 43 kW, so this was impossible on its face. The cause: TfNSW's
      <code>Charger_Type</code> column isn't always "AC" or "DC": 98 rows say
      <code>"Upcoming"</code>, and my code treated anything that wasn't literally "DC" as AC. Now
      the current type is inferred from the power when the stated type is unusable, and no connector
      is emitted at all when neither can settle it. A whole-dataset invariant asserts no site ever
      claims AC above the physical ceiling.
    </p>
  </div>

  <div class="call mine">
    <span class="h">Rejecting a match between two records 0 metres apart</span>
    <p>
      The ACT names <em>venues</em> ("Next Gen Canberra"); OpenStreetMap entries without a name tag
      fall back to the <em>operator</em> ("Exploren"). Those vocabularies never resemble each other,
      so my name-similarity penalty fired on records at the same coordinates and blocked the merge.
      Fixing it recovered five further cross-source merges nationally: the bug had been quietly
      costing matches everywhere, not just in the ACT.
    </p>
  </div>

  <div class="call mine">
    <span class="h">Fixing that would have caused a worse bug</span>
    <p>
      Tasmania and the ACT both geocode, and the geocoder resolved "Mawson Club" (10 Heard St) and
      "Southlands Shopping Centre" (12 Heard St) to the <strong>identical point</strong>. Between two
      geocoded records, distance carries no information: 0 m may mean "same site" or "the geocoder
      couldn't tell them apart". So two geocoded records now require name agreement to merge.
    </p>
  </div>

  <div class="call mine">
    <span class="h">A filter that worked everywhere except the API</span>
    <p>
      The town-level opt-in worked in the CLI and the core engine but was <strong>silently ignored
      over HTTP</strong>, because I never mapped the query parameter. I had a cross-surface
      equivalence test specifically to catch this class of divergence, and it missed, because its
      case list didn't include that parameter. Such a test only guarantees the cases you enumerate.
    </p>
  </div>

  <div class="call mine">
    <span class="h">Two charts that rendered as blank rails</span>
    <p>
      The bar charts in the first version of this write-up were <strong>completely empty</strong> and
      I published them twice without noticing. The fills were <code>&lt;span&gt;</code> elements, and
      CSS width and height don't apply to non-replaced inline elements, so all 13 rendered at 0×0.
      The automated render check passed both times (a zero-size element produces no console error
      and no failed request), and my own checks measured <em>container</em> geometry and read text
      back, which an empty rail satisfies perfectly.
    </p>
    <p>
      The lesson generalises past CSS: if the thing you're verifying is <em>drawn</em>, assert
      something about the drawing. I now check that nothing styled to have a size renders at zero
      size, which catches the whole class rather than the one instance.
    </p>
  </div>
</section>

<section id="advice">
  <h2>What I'd tell you before you start</h2>
  <ol>
    <li><strong>Settle the licence question first.</strong> It's the one decision that is expensive
      to reverse, and it is a product decision disguised as a footnote.</li>
    <li><strong>Budget real effort for reconciliation.</strong> NSW alone has more records than
      all-of-Australia OpenStreetMap, and neither is a superset. Concatenate them and you ship
      doubled pins; round coordinates to deduplicate and you merge distinct chargers in one car park.
      This is where the perceived quality of the whole thing lives.</li>
    <li><strong>Never invent a value.</strong> An unparseable rating stays null. A missing fee is
      <em>unknown</em>, not free. A town name is not a location. Guessing corrupts silently and is
      invisible in testing.</li>
    <li><strong>Look for an invariant the publisher already asserts</strong>: a stated total, count
      or date you can reconcile against. It's the cheapest possible way to make a scrape trustworthy.</li>
    <li><strong>Validate before you cache</strong>, or an error page becomes your dataset.</li>
    <li><strong>Send corrections upstream.</strong> Fixes belong in OpenStreetMap, where they benefit
      everyone and flow back on your next ingest. A private correction store forks from your sources
      and rots.</li>
    <li><strong>Say what you don't know.</strong> No pricing, no live availability, roughly one site
      in five missing entirely, and four jurisdictions where every pin came from a volunteer. A map
      that admits this is more useful than one implying uniform authority.</li>
  </ol>

  <div class="call stop">
    <span class="h">The thing open data cannot give you</span>
    <p>
      <strong>Live availability does not exist in open form.</strong> Not at any effort level: it's
      an OCPI roaming or per-operator partnership problem. If you add it later it belongs as a
      request-time overlay that is never written into your dataset, because a cached "available"
      sends a driver to an occupied charger with false confidence. That's worse than no answer.
    </p>
  </div>
</section>

</main>

<footer>
  <div class="foot">
    <p>
      <strong>Method.</strong> Every figure here comes from a real ingest run on
      6 September 2026,
      not an estimate. Source availability, licensing and record counts were verified by live HTTP
      request. Operator websites were each checked for a 2xx response, and confirmed in a real
      browser where the site blocks automated clients; unresolved ones are left blank. Coverage
      percentages are measured against published third-party baselines (Carloop, August 2026;
      EVlog, July 2026; Electric Vehicle Council <em>State of EVs</em>, September 2025), whose
      definitions of "public charging site" differ; treat the range, not a single number.
    </p>
    <p style="margin-top:16px">
      <strong>Data attribution.</strong> © OpenStreetMap contributors (ODbL 1.0) ·
      © State of New South Wales (Transport for NSW) (CC-BY 3.0 AU) ·
      © State of Victoria, Department of Energy, Environment and Climate Action (CC-BY 4.0) ·
      © State of Queensland, Department of Transport and Main Roads (CC-BY 4.0) ·
      © Australian Capital Territory (CC-BY 4.0) ·
      © State of Tasmania, Department of Natural Resources and Environment (CC-BY 4.0) ·
      Geocoding © OpenStreetMap contributors (Nominatim) (ODbL 1.0).
    </p>
    <p style="margin-top:16px">
      The aggregated dataset described here is an ODbL Derivative Database. It is not a complete
      census of Australian public charging, contains no live availability information, and should
      not be presented as authoritative.
    </p>
  </div>
</footer>

<script>
document.addEventListener('keydown', function(e) {
  if (e.key === 'Escape' && window.parent !== window) {
    window.parent.postMessage({ type: 'close-fullscreen' }, '*');
  }
});
</script>
<!-- broken-img-handler -->
<script>
(function(){
  if(window.__brokenImgHandler)return;
  window.__brokenImgHandler=true;
  var MAX=5,DELAYS=[2000,4000,8000,16000,32000];
  var TOKEN=/^__(?:IMG|EMBED)_[a-zA-Z0-9_-]+__/;
  var SENTINEL=new RegExp('^/api/published/[^/]+/(?:assets|embeds)/[^/?#]*-missing(?:[?#]|$)');
  var NOISE=/^[\s\u2018\u2019\u201c\u201d]+|[\s\u2018\u2019\u201c\u201d]+$/g;
  function canSrcEverResolve(src){
    var bare=src.replace(NOISE,'');
    return !TOKEN.test(bare)&&!SENTINEL.test(bare);
  }
  document.addEventListener('error',function(e){
    var img=e.target;
    if(!img||img.tagName!=='IMG')return;
    var liveSrc=img.getAttribute('src');
    var src=img.dataset.haOriginalSrc||liveSrc;
    if(!src)return;
    if(img.dataset.haOriginalSrc&&liveSrc&&liveSrc!==img.dataset.haOriginalSrc&&liveSrc.indexOf('_r=')<0){src=liveSrc;img.dataset.haOriginalSrc=src;img.dataset.haRetryCount='0'}
    else if(!img.dataset.haOriginalSrc){img.dataset.haOriginalSrc=src}
    var attempt=canSrcEverResolve(src)?parseInt(img.dataset.haRetryCount||'0',10):MAX;
    if(img.dataset.haPhId){var old=document.getElementById(img.dataset.haPhId);if(old)old.remove()}
    var ph=document.createElement('div');
    ph.className='ha-img-placeholder'+(attempt>=MAX?' ha-failed':'');
    ph.id='ha-ph-'+Math.random().toString(36).slice(2,9);
    var w=img.getAttribute('width');var h=img.getAttribute('height');
    if(w)ph.style.width=w+(isNaN(Number(w))?'':'px');
    else if(img.style.width)ph.style.width=img.style.width;
    else if(img.width>1)ph.style.width=img.width+'px';
    if(h)ph.style.height=h+(isNaN(Number(h))?'':'px');
    else if(img.style.height)ph.style.height=img.style.height;
    else if(img.height>1)ph.style.height=img.height+'px';
    ph.textContent=attempt>=MAX?'Image unavailable':'Loading image\u2026';
    img.dataset.haPhId=ph.id;
    if(img.dataset.haOrigDisplay==null)img.dataset.haOrigDisplay=img.style.display||'';
    img.style.display='none';
    img.insertAdjacentElement('afterend',ph);
    if(attempt<MAX){
      img.dataset.haRetryCount=String(attempt+1);
      setTimeout(function(){
        if(!img.isConnected)return;
        if(img.dataset.haOriginalSrc!==src)return;
        if(img.complete&&img.naturalWidth>0)return;
        var curSrc=img.getAttribute('src');
        if(curSrc&&curSrc.indexOf(src)!==0)return;
        var fresh=src+(src.indexOf('?')>=0?'&':'?')+'_r='+(attempt+1)+'_'+Date.now();
        img.src=fresh;
      },DELAYS[attempt]);
    }
  },true);
  document.addEventListener('load',function(e){
    var img=e.target;
    if(!img||img.tagName!=='IMG')return;
    if(img.dataset.haPhId){
      var ph=document.getElementById(img.dataset.haPhId);
      if(ph)ph.remove();
      delete img.dataset.haPhId;
      img.style.display=img.dataset.haOrigDisplay||'';
      delete img.dataset.haOrigDisplay;
      delete img.dataset.haOriginalSrc;
      delete img.dataset.haRetryCount;
    }
  },true);
})();
</script>

