Renewable energy siting
Multi-criteria siting for wind, solar and storage across Australia — renewable energy zones, transmission proximity, terrain, land use, hazards and title fragmentation in one screen.
Shared run
A published agent run. Rural lots inside the Central-West Orana Renewable Energy Zone screened on area, slope and distance to high-voltage transmission — with the seven calls that failed on the way and the map of what survived.
This is one run, on 2 September 2026, published the way it came out of the app. It is the roughest of the four published here, and it is published as it ran: half its calls failed, its own publishing checks rejected a table, and the answer it settled on carries a mistake this page names below. A page of only the runs that went smoothly would be a different product from the one we are building.
Screen the Central-West Orana REZ for solar-farm land: rural-zoned lots of 100 ha or more, slope under 5 degrees at the lot, within 5 km of a 330 kV or higher transmission line. Flag bushfire-prone land rather than excluding it, count the addresses within 1 km as a neighbour proxy, rank by area and map the top 20 with the transmission lines.
address_profilecell_profileparcel_profilereztransmission14 tool calls316s analysis5 datasets
Four filters and two flags. Three of the filters are joins the library already holds — zoning, lot area, slope — and the fourth is a distance to a line whose voltage has to be read before the distance means anything. The two flags are the interesting instruction: count the neighbours rather than exclude on them, and mark bushfire-prone land rather than dropping it. That last clause exists because the earlier version of this brief excluded bushfire-prone land, and on rural land in this part of the state that exclusion takes the answer with it.
7 calls failed and the agent recovered within the same run. Failures are shown because they happened.
Seven of the fourteen calls failed, and they are worth reading in order. A first query referenced a slope column the parcel table does not carry. A second hit a DuckDB restriction on non-inner joins inside a subquery. Two timed out at ninety seconds, and one of those was the neighbour count — a straight-line radius join between every candidate lot and every mapped address is a cross join unless it is written carefully. A query tool call failed on a malformed lake URL. Then the render of the first table was rejected by the app's own publishing checks, for showing a reader a column called H3 R9.
After that it worked, and the last four calls are the run. That shape — six failures, a rejected render, then a clean finish — is what an agent recovering looks like, and it is the reason the steps above are on the page at all.
| Lot | Plan | Suburb | LGA | Lot area (ha) | Slope (degrees) | Zone | Bushfire flag | Nearest ≥330 kV line (km) | Addresses within 1 km |
|---|---|---|---|---|---|---|---|---|---|
| 4 | DP1210670 | Cope | Mid-Western | 749.9 | 0.4 | RU1 | bushfire-prone | 2.84 | 10 |
| 167 | DP754290 | Spicers Creek | Dubbo | 562.3 | 2.83 | RU1 | bushfire-prone | 1.58 | 2 |
| 201 | DP1286184 | Wuuluman | Dubbo | 554.4 | 2.38 | RU1 | bushfire-prone | 1.14 | 0 |
| 11 | DP877624 | Bodangora | Dubbo | 526.6 | 3.1 | RU1 | bushfire-prone | 3.52 | 1 |
| 24 | DP756912 | Euchareena | Dubbo | 515 | 2.16 | RU1 | bushfire-prone | 3.04 | 0 |
| 2 | DP219791 | Bodangora | Dubbo | 472.6 | 1.71 | RU1 | bushfire-prone | 2.4 | 1 |
| 32 | DP622471 | Wuuluman | Dubbo | 458.5 | 3.36 | RU1 | bushfire-prone | 0.53 | 2 |
| 1 | DP750776 | Goolma | Dubbo | 446.4 | 1.85 | RU1 | bushfire-prone | 1.75 | 1 |
| 2 | DP835442 | Wuuluman | Dubbo | 437.2 | 1.94 | RU1 | bushfire-prone | 0.67 | 0 |
| 2 | DP879723 | Two Mile Flat | Mid-Western | 430 | 2.95 | RU1 | bushfire-prone | 2.68 | 0 |
| 29 | DP1079852 | Guntawang | Mid-Western | 404.4 | 2.21 | RU1 | bushfire-prone | 4.32 | 0 |
| 227 | DP755442 | Ulan | Mid-Western | 392.2 | 3.43 | RU1 | bushfire-prone | 3.16 | 2 |
| 34 | DP756866 | Stuart Town | Dubbo | 392 | 4.16 | RU1 | bushfire-prone | 2.31 | 1 |
| 22 | DP754290 | Bodangora | Dubbo | 391.4 | 2.07 | RU1 | bushfire-prone | 0.06 | 0 |
| 217 | DP756886 | Mumbil | Dubbo | 390.7 | 2.45 | RU1 | bushfire-prone | 1.79 | 0 |
| 55 | DP750760 | Bodangora | Dubbo | 390.3 | 2.31 | RU1 | bushfire-prone | 1.57 | 0 |
| 18 | DP756865 | Guntawang | Mid-Western | 380.7 | 2.51 | RU1 | bushfire-prone | 3.48 | 0 |
| 1 | DP756902 | Twelve Mile | Mid-Western | 380 | 3.37 | RU1 | bushfire-prone | 1.41 | 4 |
| 69 | DP750773 | Ulan | Mid-Western | 361.9 | 2.17 | RU1 | bushfire-prone | 4.94 | 4 |
| 271 | DP836190 | Euchareena | Dubbo | 347.4 | 4.42 | RU1 | bushfire-prone | 1.79 | 0 |
The twenty largest lots run from 347.4 to 749.9 hectares, sit between 0.4 and 4.42 degrees of slope, and are 0.06 to 4.94 kilometres from a qualifying line. Thirteen are in Dubbo and seven in Mid-Western. The neighbour proxy is between zero and ten addresses within a kilometre, which is the number a developer would look at first and the one the run is least able to stand behind.
The screen identified 237 qualifying lots inside the declared NSW EnergyCo Central-West Orana Renewable Energy Zone. The largest lot is 749.9 hectares, and 227 lots are flagged as bushfire-prone rather than excluded.
The shortlist is ranked by lot area. The map shows the 20 largest lots, grouped into areas of about 10 hectares, together with nearby transmission lines operating at 330 kV or higher.
The neighbour proxy counts mapped addresses within a 1 kilometre straight-line radius. Lots must be within 5 kilometres of a qualifying transmission line. Being inside the Renewable Energy Zone does not imply planning approval, development consent or available grid-connection capacity.
One line of that is wrong. The agent describes the map's cells as "about 10 hectares"; they are H3 resolution-8 cells, which at this latitude are closer to ninety hectares each. Nothing else on the page depends on the figure — the areas in the table come from the lots, not from the cells — but it is exactly the kind of quiet error that a run showing its working lets you catch, and it stays on the page rather than being edited out from under the run that produced it.
Ten cells, six of which failed. They are here in full, in the order they ran.
python3 - <<'EOF'
import zn
con=zn.lake_con()
PP=zn.read('parcel_profile')
REZH=zn.read('rez', file='rez_h3r9')
REZ=zn.read('rez', file='rez')
LINES=zn.read('transmission', file='lines_h3r9')
ADDR=zn.read('address_profile')
# Filter declared NSW EnergyCo CWO, rural parcels, then transmission proximity and address proxy.
base = f"""
WITH rez_cells AS (
SELECT DISTINCT h.h3_r9
FROM {REZH} h JOIN {REZ} z USING (rez_id)
WHERE z.authority = 'nsw_energyco'
AND z.status = 'declared'
AND z.name = 'Central-West Orana'
AND NOT z.offshore
),
candidates AS (
SELECT p.parcel_id, p.lot, p.plan_label, p.lga_name, p.suburb, p.postcode,
p.lat, p.lng, p.h3_r9, p.area_m2, p.zone_code, p.zone_name,
p.slope_deg, p.bushfire_prone,
p.h3_r9 AS parcel_cell
FROM {PP} p
SEMI JOIN rez_cells r USING (h3_r9)
WHERE p.lot <> ''
AND p.area_m2 >= 1000000
AND p.zone_group = 'Rural'
AND p.slope_deg < 5
),
line_cells AS (
SELECT DISTINCT h3_r9, voltage_kv
FROM {LINES}
WHERE voltage_kv >= 330
),
with_line AS (
SELECT c.*,
(SELECT min(h3_great_circle_distance(h3_cell_to_lat(l.h3_r9), h3_cell_to_lng(l.h3_r9), c.lat, c.lng, 'km'))
FROM line_cells l
WHERE l.h3_r9 IN (SELECT unnest(h3_grid_disk(c.parcel_cell, 29)))) AS line_distance_km
FROM candidates c
),
qualified AS (
SELECT * FROM with_line WHERE line_distance_km <= 5
),
neighbours AS (
SELECT q.parcel_id,
count(DISTINCT a.gnaf_pid) AS addresses_within_1km
FROM qualified q
LEFT JOIN {ADDR} a
ON a.h3_r9 IN (SELECT unnest(h3_grid_disk(q.parcel_cell, 6)))
AND h3_great_circle_distance(q.lat, q.lng, a.lat, a.lng, 'km') <= 1
GROUP BY q.parcel_id
)
SELECT q.parcel_id, q.lot, q.plan_label, q.lga_name, q.suburb, q.postcode,
round(q.area_m2 / 10000.0, 1) AS area_ha,
round(q.slope_deg, 2) AS slope_deg,
q.zone_code, q.zone_name,
CASE WHEN q.bushfire_prone IS TRUE THEN 'bushfire-prone' ELSE 'not mapped as bushfire-prone' END AS bushfire_flag,
round(q.line_distance_km, 2) AS transmission_distance_km,
COALESCE(n.addresses_within_1km, 0) AS addresses_within_1km,
q.lat, q.lng, q.h3_r9
FROM qualified q LEFT JOIN neighbours n USING (parcel_id)
ORDER BY q.area_m2 DESC
"""
# Entity set first
zn.write_out(con, 'cwo_solar_land_candidates', base)
# top 20 derived from the staged entity set, preserving exact output columns
con.execute("""
COPY (SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv') ORDER BY area_ha DESC LIMIT 20)
TO '/workspace/out/cwo_solar_land_top20.csv' (HEADER)
""")
print(con.sql("SELECT count(*) AS candidates, count(*) FILTER (WHERE bushfire_flag='bushfire-prone') AS bushfire_flagged, max(area_ha) AS largest_ha FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv')").fetchall())
print(con.sql("SELECT count(*) AS top20 FROM read_csv_auto('/workspace/out/cwo_solar_land_top20.csv')").fetchall())
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con()
PP=zn.read('parcel_profile')
CP=zn.read('cell_profile')
REZH=zn.read('rez', file='rez_h3r9')
REZ=zn.read('rez', file='rez')
LINES=zn.read('transmission', file='lines_h3r9')
ADDR=zn.read('address_profile')
entities = f"""
WITH rez_cells AS (
SELECT DISTINCT h.h3_r9
FROM {REZH} h JOIN {REZ} z USING (rez_id)
WHERE z.authority = 'nsw_energyco' AND z.status = 'declared'
AND z.name = 'Central-West Orana' AND NOT z.offshore
), candidates AS (
SELECT p.parcel_id, p.lot, p.plan_label, p.lga_name, p.suburb, p.postcode,
p.lat, p.lng, p.h3_r9, p.area_m2, p.zone_code, p.zone_name,
c.slope_deg, c.bushfire_category, p.h3_r9 AS parcel_cell
FROM {PP} p
JOIN {CP} c USING (h3_r9)
SEMI JOIN rez_cells r USING (h3_r9)
WHERE p.lot <> '' AND p.area_m2 >= 1000000
AND p.zone_group = 'Rural' AND c.slope_deg < 5
), line_cells AS (
SELECT DISTINCT h3_r9, voltage_kv FROM {LINES} WHERE voltage_kv >= 330
), line_dist AS (
SELECT c.parcel_id,
min(h3_great_circle_distance(h3_cell_to_lat(l.h3_r9), h3_cell_to_lng(l.h3_r9), c.lat, c.lng, 'km')) AS line_distance_km
FROM candidates c
JOIN line_cells l ON l.h3_r9 IN (SELECT unnest(h3_grid_disk(c.parcel_cell, 29)))
GROUP BY c.parcel_id
), qualified AS (
SELECT c.*, d.line_distance_km
FROM candidates c JOIN line_dist d USING (parcel_id)
WHERE d.line_distance_km <= 5
), neighbours AS (
SELECT q.parcel_id, count(DISTINCT a.gnaf_pid) AS addresses_within_1km
FROM qualified q LEFT JOIN {ADDR} a
ON a.h3_r9 IN (SELECT unnest(h3_grid_disk(q.parcel_cell, 6)))
AND h3_great_circle_distance(q.lat, q.lng, a.lat, a.lng, 'km') <= 1
GROUP BY q.parcel_id
)
SELECT q.parcel_id, q.lot, q.plan_label, q.lga_name, q.suburb, q.postcode,
round(q.area_m2 / 10000.0, 1) AS area_ha,
round(q.slope_deg, 2) AS slope_deg, q.zone_code, q.zone_name,
CASE WHEN q.bushfire_category IS NOT NULL THEN 'bushfire-prone' ELSE 'not mapped as bushfire-prone' END AS bushfire_flag,
round(q.line_distance_km, 2) AS transmission_distance_km,
COALESCE(n.addresses_within_1km, 0) AS addresses_within_1km,
q.lat, q.lng, q.h3_r9
FROM qualified q LEFT JOIN neighbours n USING (parcel_id)
ORDER BY q.area_m2 DESC
"""
zn.write_out(con, 'cwo_solar_land_candidates', entities)
con.execute("""
COPY (SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv') ORDER BY area_ha DESC LIMIT 20)
TO '/workspace/out/cwo_solar_land_top20.csv' (HEADER)
""")
print(con.sql("SELECT count(*) AS candidates, count(*) FILTER (WHERE bushfire_flag='bushfire-prone') AS bushfire_flagged, max(area_ha) AS largest_ha FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv')").fetchall())
print(con.sql("SELECT count(*) AS top20 FROM read_csv_auto('/workspace/out/cwo_solar_land_top20.csv')").fetchall())
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con(); PP=zn.read('parcel_profile'); CP=zn.read('cell_profile'); REZH=zn.read('rez',file='rez_h3r9'); REZ=zn.read('rez',file='rez'); LINES=zn.read('transmission',file='lines_h3r9'); ADDR=zn.read('address_profile')
sql=f"""
WITH rez_cells AS (SELECT DISTINCT h.h3_r9 FROM {REZH} h JOIN {REZ} z USING (rez_id) WHERE z.authority='nsw_energyco' AND z.status='declared' AND z.name='Central-West Orana' AND NOT z.offshore),
candidates AS (SELECT p.parcel_id,p.lot,p.plan_label,p.lga_name,p.suburb,p.postcode,p.lat,p.lng,p.h3_r9,p.area_m2,p.zone_code,p.zone_name,c.slope_deg,c.bushfire_category FROM {PP} p JOIN {CP} c USING(h3_r9) SEMI JOIN rez_cells r USING(h3_r9) WHERE p.lot<>'' AND p.area_m2>=1000000 AND p.zone_group='Rural' AND c.slope_deg<5),
radial AS (SELECT c.*, unnest(h3_grid_disk(c.h3_r9,29)) AS line_cell, unnest(h3_grid_disk(c.h3_r9,6)) AS addr_cell FROM candidates c),
line_dist AS (SELECT r.parcel_id,min(h3_great_circle_distance(h3_cell_to_lat(l.h3_r9),h3_cell_to_lng(l.h3_r9),r.lat,r.lng,'km')) AS line_distance_km FROM radial r JOIN {LINES} l ON l.h3_r9=r.line_cell AND l.voltage_kv>=330 GROUP BY r.parcel_id),
qualified AS (SELECT c.*,d.line_distance_km FROM candidates c JOIN line_dist d USING(parcel_id) WHERE d.line_distance_km<=5),
addr_counts AS (SELECT q.parcel_id,count(DISTINCT a.gnaf_pid) AS addresses_within_1km FROM qualified q JOIN radial r USING(parcel_id) LEFT JOIN {ADDR} a ON a.h3_r9=r.addr_cell AND h3_great_circle_distance(q.lat,q.lng,a.lat,a.lng,'km')<=1 GROUP BY q.parcel_id)
SELECT q.parcel_id,q.lot,q.plan_label,q.lga_name,q.suburb,q.postcode,round(q.area_m2/10000.0,1) AS area_ha,round(q.slope_deg,2) AS slope_deg,q.zone_code,q.zone_name,CASE WHEN q.bushfire_category IS NOT NULL THEN 'bushfire-prone' ELSE 'not mapped as bushfire-prone' END AS bushfire_flag,round(q.line_distance_km,2) AS transmission_distance_km,coalesce(a.addresses_within_1km,0) AS addresses_within_1km,q.lat,q.lng,q.h3_r9 FROM qualified q LEFT JOIN addr_counts a USING(parcel_id) ORDER BY q.area_m2 DESC
"""
zn.write_out(con,'cwo_solar_land_candidates',sql)
con.execute("COPY (SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv') ORDER BY area_ha DESC LIMIT 20) TO '/workspace/out/cwo_solar_land_top20.csv' (HEADER)")
print(con.sql("SELECT count(*) AS candidates,count(*) FILTER(WHERE bushfire_flag='bushfire-prone') AS flagged,max(area_ha) AS largest_ha FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv')").fetchall())
print(con.sql("SELECT count(*) FROM read_csv_auto('/workspace/out/cwo_solar_land_top20.csv')").fetchall())
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con(); PP=zn.read('parcel_profile'); CP=zn.read('cell_profile'); REZH=zn.read('rez',file='rez_h3r9'); REZ=zn.read('rez',file='rez'); LINES=zn.read('transmission',file='lines_h3r9'); ADDR=zn.read('address_profile')
cand=f"""SELECT p.parcel_id,p.lot,p.plan_label,p.lga_name,p.suburb,p.postcode,p.lat,p.lng,p.h3_r9,p.area_m2,p.zone_code,p.zone_name,c.slope_deg,c.bushfire_category
FROM {PP} p JOIN {CP} c USING(h3_r9)
WHERE p.lot<>'' AND p.area_m2>=1000000 AND p.zone_group='Rural' AND c.slope_deg<5
AND p.h3_r9 IN (SELECT DISTINCT h.h3_r9 FROM {REZH} h JOIN {REZ} z USING(rez_id) WHERE z.authority='nsw_energyco' AND z.status='declared' AND z.name='Central-West Orana' AND NOT z.offshore)"""
zn.write_out(con,'cwo_candidates_base',cand)
con.execute("CREATE TEMP TABLE c AS SELECT * FROM read_csv_auto('/workspace/out/cwo_candidates_base.csv')")
con.execute("""CREATE TEMP TABLE q AS SELECT c.*, min(h3_great_circle_distance(h3_cell_to_lat(l.h3_r9),h3_cell_to_lng(l.h3_r9),c.lat,c.lng,'km')) AS transmission_distance_km FROM c CROSS JOIN LATERAL (SELECT unnest(h3_grid_disk(c.h3_r9,29)) AS h3_r9) d JOIN %s l ON l.h3_r9=d.h3_r9 AND l.voltage_kv>=330 GROUP BY ALL HAVING min(h3_great_circle_distance(h3_cell_to_lat(l.h3_r9),h3_cell_to_lng(l.h3_r9),c.lat,c.lng,'km'))<=5"""%LINES)
con.execute("COPY (SELECT * FROM q ORDER BY area_m2 DESC) TO '/workspace/out/cwo_qualified.csv' (HEADER)")
con.execute("CREATE TEMP TABLE qq AS SELECT * FROM q")
# Address proxy using each qualified parcel's r9 neighbouring cells, no cross-product with line cells
con.execute("""COPY (SELECT q.parcel_id,q.lot,q.plan_label,q.lga_name,q.suburb,q.postcode,round(q.area_m2/10000.0,1) AS area_ha,round(q.slope_deg,2) AS slope_deg,q.zone_code,q.zone_name,CASE WHEN q.bushfire_category IS NOT NULL THEN 'bushfire-prone' ELSE 'not mapped as bushfire-prone' END AS bushfire_flag,round(q.transmission_distance_km,2) AS transmission_distance_km,count(DISTINCT a.gnaf_pid) AS addresses_within_1km,q.lat,q.lng,q.h3_r9 FROM qq q LEFT JOIN LATERAL (SELECT unnest(h3_grid_disk(q.h3_r9,6)) AS h3_r9) r ON true LEFT JOIN %s a ON a.h3_r9=r.h3_r9 AND h3_great_circle_distance(q.lat,q.lng,a.lat,a.lng,'km')<=1 GROUP BY ALL ORDER BY q.area_m2 DESC) TO '/workspace/out/cwo_solar_land_candidates.csv' (HEADER)"""%ADDR)
con.execute("COPY (SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv') ORDER BY area_ha DESC LIMIT 20) TO '/workspace/out/cwo_solar_land_top20.csv' (HEADER)")
print(con.sql("SELECT count(*),count(*) FILTER(WHERE bushfire_flag='bushfire-prone'),max(area_ha) FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv')").fetchall())
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con(); ADDR=zn.read('address_profile')
con.execute("CREATE TEMP TABLE qq AS SELECT * FROM read_csv_auto('/workspace/out/cwo_qualified.csv')")
con.execute("""COPY (SELECT q.parcel_id,q.lot,q.plan_label,q.lga_name,q.suburb,q.postcode,round(q.area_m2/10000.0,1) AS area_ha,round(q.slope_deg,2) AS slope_deg,q.zone_code,q.zone_name,CASE WHEN q.bushfire_category IS NOT NULL THEN 'bushfire-prone' ELSE 'not mapped as bushfire-prone' END AS bushfire_flag,round(q.transmission_distance_km,2) AS transmission_distance_km,count(DISTINCT a.gnaf_pid) AS addresses_within_1km,q.lat,q.lng,q.h3_r9 FROM qq q LEFT JOIN LATERAL (SELECT unnest(h3_grid_disk(q.h3_r9,6)) AS h3_r9) r ON true LEFT JOIN %s a ON a.h3_r9=r.h3_r9 AND h3_great_circle_distance(q.lat,q.lng,a.lat,a.lng,'km')<=1 GROUP BY q.parcel_id,q.lot,q.plan_label,q.lga_name,q.suburb,q.postcode,q.area_m2,q.slope_deg,q.zone_code,q.zone_name,q.bushfire_category,q.transmission_distance_km,q.lat,q.lng,q.h3_r9 ORDER BY area_ha DESC) TO '/workspace/out/cwo_solar_land_candidates.csv' (HEADER)"""%ADDR)
con.execute("COPY (SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv') ORDER BY area_ha DESC LIMIT 20) TO '/workspace/out/cwo_solar_land_top20.csv' (HEADER)")
print(con.sql("SELECT count(*) AS candidates,count(*) FILTER(WHERE bushfire_flag='bushfire-prone') AS flagged,max(area_ha) AS largest_ha FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv')").fetchall())
EOFWITH q AS (SELECT * FROM read_csv_auto('/workspace/out/cwo_qualified.csv')),
ring AS (SELECT q.parcel_id, unnest(h3_grid_disk(q.h3_r9,6)) AS h3_r9 FROM q),
ac AS (SELECT r.parcel_id, count(DISTINCT a.gnaf_pid) AS addresses_within_1km
FROM ring r JOIN read_parquet(['https://placeholder']) a ON false
GROUP BY r.parcel_id)
SELECT * FROM acpython3 - <<'EOF'
import duckdb
con=duckdb.connect()
print(con.sql("SELECT lga_name,count(*) FROM read_csv_auto('/workspace/out/cwo_qualified.csv') GROUP BY 1 ORDER BY 2 DESC").fetchall())
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con(); A=zn.read('address_profile')
con.execute("CREATE TEMP TABLE q AS SELECT * FROM read_csv_auto('/workspace/out/cwo_qualified.csv')")
sql=f"""SELECT q.parcel_id,q.lot,q.plan_label,q.lga_name,q.suburb,q.postcode,round(q.area_m2/10000.0,1) AS area_ha,round(q.slope_deg,2) AS slope_deg,q.zone_code,q.zone_name,CASE WHEN q.bushfire_category IS NOT NULL THEN 'bushfire-prone' ELSE 'not mapped as bushfire-prone' END AS bushfire_flag,round(q.transmission_distance_km,2) AS transmission_distance_km,count(DISTINCT a.gnaf_pid) AS addresses_within_1km,q.lat,q.lng,q.h3_r9
FROM q LEFT JOIN {A} a ON a.lga_name IN ('Mid-Western','Dubbo') AND a.lat BETWEEN q.lat-0.012 AND q.lat+0.012 AND a.lng BETWEEN q.lng-0.015 AND q.lng+0.015 AND h3_great_circle_distance(q.lat,q.lng,a.lat,a.lng,'km')<=1
GROUP BY q.parcel_id,q.lot,q.plan_label,q.lga_name,q.suburb,q.postcode,q.area_m2,q.slope_deg,q.zone_code,q.zone_name,q.bushfire_category,q.transmission_distance_km,q.lat,q.lng,q.h3_r9"""
zn.write_out(con,'cwo_solar_land_candidates',sql)
zn.write_out(con,'cwo_solar_land_top20',"SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv') ORDER BY area_ha DESC LIMIT 20")
print(con.sql("SELECT count(*) AS candidates,count(*) FILTER(WHERE bushfire_flag='bushfire-prone') AS flagged,max(area_ha) AS largest_ha FROM read_csv_auto('/workspace/out/cwo_solar_land_candidates.csv')").fetchall())
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con()
zn.write_out(con,'cwo_solar_land_top20_reader',"SELECT parcel_id,lot,plan_label,lga_name,suburb,postcode,area_ha,slope_deg,zone_code,zone_name,bushfire_flag,transmission_distance_km,addresses_within_1km,lat,lng FROM read_csv_auto('/workspace/out/cwo_solar_land_top20.csv') ORDER BY area_ha DESC")
EOFpython3 - <<'EOF'
import zn
con=zn.lake_con(); L=zn.read('transmission',file='lines_h3r9')
sql=f"""WITH top AS (SELECT * FROM read_csv_auto('/workspace/out/cwo_solar_land_top20.csv')),
lot_cells AS (SELECT DISTINCT h3_cell_to_parent(h3_r9,8) AS h3_r8 FROM top),
line_cells AS (SELECT DISTINCT h3_cell_to_parent(l.h3_r9,8) AS h3_r8 FROM top t CROSS JOIN LATERAL (SELECT unnest(h3_grid_disk(t.h3_r9,29)) AS h3_r9) d JOIN {L} l ON l.h3_r9=d.h3_r9 AND l.voltage_kv>=330),
allc AS (SELECT h3_r8, 1 AS lot_cells, 0 AS line_cells FROM lot_cells UNION ALL SELECT h3_r8,0,1 FROM line_cells)
SELECT h3_r8, max(lot_cells) AS top_lot, max(line_cells) AS transmission_line, max(lot_cells)+2*max(line_cells) AS map_value FROM allc GROUP BY h3_r8"""
zn.write_out(con,'cwo_top20_transmission_map',sql)
EOFThe last one is the map. It takes the twenty lots, walks a grid disk out from each, keeps the transmission cells inside it that carry 330 kV or more, and unions the two sets into one layer with a code for lot, line and both. That is why the cells are the shape they are: the agent had a cell-indexed transmission layer and a cell-indexed lot, and joining them at a shared resolution is the cheap way to ask "is there a line near this".
A cell is not a lot. Each of the twenty lots is drawn as the single H3 cell its centroid falls in, so a 749-hectare lot and a 347-hectare lot are the same size on the map. The map shows where the shortlist is and where the lines are. It does not show the shape or the extent of anything.
The distance is straight-line, not a connection. Being 60 metres from a 330 kV line says nothing about whether there is capacity on it, whether a connection point exists, or what the queue looks like. The agent says this itself in its last paragraph, and it is the single largest gap between this screen and a real siting decision.
Slope is one number for the whole lot. A mean slope under five degrees is compatible with a gully through the middle of the site. Cut-and-fill, aspect and the actual buildable envelope are not in this screen.
The neighbour count is addresses, not people or dwellings. It counts mapped address points inside a one-kilometre circle. An address point is not a house, a shed with an address is counted, and a dwelling that has never been given an address point is not. Zero neighbours means zero mapped addresses.
Being inside the zone is not a permission. The Central-West Orana Renewable Energy Zone is a declared area, not a consent pathway. Nothing here has been checked against biodiversity offsets, Aboriginal cultural heritage, agricultural land classification, or the local environmental plan's own tests.
Everything on the shortlist is bushfire-prone. That is the honest result of the prompt asking for a flag rather than an exclusion, and it is why the prompt asks for a flag rather than a filter. Bushfire-prone mapping across this part of the state is close to universal on rural land, so as a filter it removes the answer.
The zone is Central-West Orana because the prompt named it. The same screen runs over any declared REZ, any set of council areas, or any polygon you bring, with your own area floor, slope ceiling, voltage threshold and buffer. If you want to see it against yours, request access — or read how this is used for renewable energy siting and by renewable energy developers.
Multi-criteria siting for wind, solar and storage across Australia — renewable energy zones, transmission proximity, terrain, land use, hazards and title fragmentation in one screen.
For siting and land access teams — renewable energy zones, transmission proximity, terrain, land use, hazards and title fragmentation in one screen, plus the community profile for consultation.
Zenancy is in private preview with Group 2 reporters and their advisers.
Request access