Placement and routing
Placement
Freerouting routes; it does not place. placer.place(netlist, seed=1) is a simulated-annealing placer whose cost is the half-perimeter wirelength of every net (ground nets weighted 0.25: a weight kept from when the cost assumed a pour that did not yet exist, and left because every board in the corpus was placed with it; whether it should stay is the pour's question now, below), plus a charge for parts in each other's space graded by what is touching (copper on copper 4000 per mm², body on body 400, courtyard on courtyard 20), plus a pull that drags connectors to the nearest board edge (weight 8). Since 19 Sep 2026 every courtyard is grown by a quarter of a millimetre on each side before that charge is measured -- placer.SPACING_MM = 0.5, the air between parts, measured twice on the 59-design replay (+4/0 on 16 Sep, +3/0 on 19 Sep, no board lost) -- so parts are placed half a millimetre apart rather than packed to courtyards touching, which is where a wirelength cost with no density term always ends up; a decoupling cap or crystal and the chip it serves are one unit and sit on their real courtyards. SOLDERMASK_SPACING_MM holds every board to another value (0 is the placer as it was; auto works the air out from the board's room, placer.spacing(), which was tried the same day and came back two boards short of the constant). Parts start on a coarse grid; 20,000 steps of Gaussian jitter and 90° flips cool from half the board's larger dimension to 0.02 mm. Parts stay 1.5 mm inside the edge.
The placer is seeded — --seed on the command line, 1 in the app — so a board can be regenerated exactly. Its report (cost, worst_overlap_mm2, off_board_mm, antenna_copper_mm2, clean) is printed on the command line.
Six placements are annealed per build, not one, and the routability model — trained on what the router did with boards like these — puts them in the order the router should try them. The anneal's cost is incremental: only the moved part's nets and overlaps are re-summed each step, the same number to 1e-12 and eight times fewer operations, so six cost about what one used to.
The other half of that cost was a package's bounding box, which is what the overlap test asks for twice per pair and was being recomputed from its pads every time — 4.7 million times per anneal on a thirty-part board, 60% of the whole stage. A package is frozen, so the answer cannot change; it is now computed once per package. The placements are identical, seed for seed, because nothing about the search changed:
| parts | before | after |
|---|---|---|
| 8 | 6.6s | 1.8s |
| 20 | 17.2s | 4.4s |
| 30 | 26.0s | 6.4s |
| 60 | 52.3s | 12.7s |
A placement can be an input. Some boards are not this pipeline's to arrange. A keyboard's switches sit on 18 mm centres because switches are 18 mm; a connector sits over the hole the case has for it; a board that came out of a generator carries that geometry already. placer.candidates(nl, fixed={...}) — and anneal, place and workspace.realise — pin those parts where the caller put them. A pinned part is never moved, turned, centred or seated at an edge, and placement_notes passes over it: the complaint it makes is that the placer tried to seat a connector and could not, and nobody tried to seat a pinned one. But a pinned part is still in the cost, so the parts that are free are placed around it rather than in ignorance of it.
Pin every part and there is one placement, so candidates() returns one however many were asked for — still rated by the model, because what the router will make of a placement is worth knowing even when it is the only one on offer. Such a route is recorded in the corpus as as pinned, so a model judged on the placements the placer makes is not judged on placements it did not make. With nothing pinned, the anneal is the anneal it always was, seed for seed.
Antenna keepouts. A radio module's footprint says where its antenna is the same way it says which way the module faces: the body reaches past the pads on one side. That stretch — the full body width, from where the pads stop to the end of the body — is a copper keepout on both layers: dsn.py writes it into the structure block where freerouting reads it, kicad.py writes it as a rule area so KiCad's DRC holds a later edit to the same rule, and the board's notes say what was kept clear (the 19.0×7.1 mm past its pads on the side it faces). A module whose footprint does not say which way it faces gets no keepout and a note saying so, rather than a guess. Connectors and ICs get none: a USB-C shell reaches past its pads too, and copper under a shell is fine.
Since 25 Sep 2026 the placer honours the zone too; until then it was drawn for every stage after the placer and read by none before it. On the front a module's body already covers its own antenna, but a surface-mount module is in nobody's way on the other side, so the back under an antenna was the emptiest room on the board beside the pins everything is wired to — and 047-esp32s3-battery in the 24 Sep Opus 5.5 drive shipped an LDO there, its GND and VSYS pads inside a zone tracks, vias and pour may not enter, which were the two connections that failed the board. placer.antenna_charge() now charges the zone as a body on both sides: another part's copper in it at the short's 4000 per mm², its body at 400, whichever side it is on. _cost() sums it and _Index.local() charges a part's share — including the whole zone when the part that moved is the module carrying it — and tests/test_rubric.py holds the two together at 1e-9 through module moves and flips. The steps after the anneal honour it as well: edge seating will not bring a module's antenna over a part, and centring is not taken where a clamped part would end up under one. What is left is said: copper in a zone is a gap naming the part, its side and its pads (placer.antenna_gaps()), a body without copper there is a note, and report() carries antenna_copper_mm2 and is not clean with any. A board with no antenna builds none of this and anneals draw for draw as before; SOLDERMASK_ANTENNA_KEEPOUT=0 turns it off, for a control arm. The zone is still inferred from the footprint alone: a module whose antenna is a trace on its own PCB (both Picos, platforms.py) has a keepout declared there that nothing in the build reads, and gets none.
Both sides. A placement carries a side, and a surface-mount part may go on the back. On the back a part is its footprint mirrored about its own vertical axis and then turned by its angle, counter-clockwise as seen from the front — one convention, held in Placement.local(), that every writer and reader goes through. It was measured rather than chosen: freerouting reads a Specctra back placement exactly that way (a pad at (−0.78, 0.5) in its footprint lands at (+0.78, 0.5) from the origin, and at 90° at (−0.5, 0.78)), and KiCad stores a back footprint's pads already mirrored in the placed frame and rotates them as it rotates a front footprint's, so kicad.write() writes the mirrored coordinates on the B.* layers and kicadpcb.read() mirrors them back. The Gerbers put a back pad's copper, paste and mask on the B files and its outline and designator, mirrored, on B.SilkS; the assembly file marks it Bottom at the rotation the board file stores, which is what KiCad's own position export gives one; the pin walk knows which layer a pad's copper is on, so a wire passing under a back pad on the front copper reaches nothing.
Who may go: a part with no lead through the board — a through-hole part is on both sides wherever it is — and nothing a person has to reach, so a connector, a switch, an LED and a module with an antenna stay on the front (placer.back_ok). When: only where the front is out of room. fit_board() tries the back before a bigger board — the surface-mount parts that may go there split between the sides, and a board whose fuller side then fits at the density limit is not grown — and the anneal offers a flip only on such a board, so a board with room on the front never pays a fab's second assembly setup. The flip is decided on the draw the turn is decided on, so a board that may not use its back anneals draw for draw as it did before it could. The cost charges BACK_W (4 mm of wirelength) per part on the back and SIDE_NET_W (1 mm, at the net's weight) per net with surface-mount pins on both sides — the via the router will have to drill — and _Index.local() charges the same, which tests/test_sides.py holds at 1e-9 through flips. Two parts on opposite sides are not in each other's way; a through-hole part is in everyone's. constraints.board.sides = 1 holds a board to one side, and a prompt that asks for one lands there; the note names the parts that went on the back and says assembly is two-sided.
Routing
Routing is done by freerouting 2.4.1, a Java program, through the Specctra DSN/SES round trip that KiCad, Altium and every autorouter still speak.
dsn.py writes the board with two signal layers, F.Cu and B.Cu, and the design rules measured from real boards: via 0.60 mm pad on a 0.30 mm drill, and a clearance of 0.20 mm unless the parts on the board are closer than that. Coordinates are 0.1 µm units with the origin at the board's bottom-left, y up.
Clearance is the board's, not the pipeline's. dsn.rules() reads it off the parts: the smallest gap between two pads on different nets inside any one footprint, when that is less than the 0.20 mm convention, and never below the fab's floor (priors.FAB_CLEARANCE_MIN_MM, 0.127 mm, JLCPCB's stated 5 mil for two layers in 1 oz copper). A 0.5 mm-pitch package with 0.3 mm pads has 0.2 mm between them and changes nothing; a 1.00 mm header with 0.85 mm pads has 0.15 mm and pulls the whole board to 0.15 mm. Two shapes with one pad name are one pad, pads on one net may touch, and two pads on no net are nobody's gap. The rule the board was routed at is on the board's notes when it is not the convention (routed at 0.15mm clearance rather than the 0.2mm convention: J1 (PinHeader_1x04_P1.00mm) has pads on different nets 0.15mm apart, and the fab spaces copper down to 0.127mm), in board.json under routing.clearance_mm, in the corpus label (Outcome.clearance_mm), and in the .kicad_pro beside the board, so KiCad's DRC measures the board against the rule the router was given rather than failing every trace it was told it could draw. A part whose pads are closer than the fab can space copper stops the rule at the floor and is named as the reason the violations at its pads are the footprint's.
Why this exists: the router counts a pad pair closer than the rule as a clearance violation, and a violation inside a footprint is one no placement can route away. Of the finished open-hardware boards the corpus learns from, the ones whose parts sit between the floor and the convention never routed clean under the convention — 31 of 32 boards with three or more routes, and 1 of 33 routes of the human's own placement — whatever the placement. tools/rules_ab.py routes a sample of those boards under the convention, under their own rule and at the floor, with the human's placement and one of this pipeline's anneals each, and prints the table. On 60 such boards, 120 placements paired: 3 routed clean at 0.2 mm, 30 at the board's own rule, 38 at the floor, none went the other way, and violations per route fell from 51 to 18. On 51 boards whose parts allow 0.2 mm the floor changed nothing (2 gained, 1 lost of 100 pairs), which is why the rule stays at the convention wherever the parts allow it. What is left under the right rule is nets unrouted on a dense board — 66 of the 90 placements still dirty — so on fine-pitch boards the placement is the next constraint, not the rule.
Track width is per net. A net that declares a current_a is sized to IPC-2221 by electrical.py and gets its own DSN class at that width (soldermask_781um for a 2 A rail); every other net stays in the soldermask class at the measured 0.25 mm median. The prior is a floor and the physics only ever raises it — 10 mA does not buy a 0.01 mm trace — so a board that declares nothing anywhere emits exactly one class and exactly the DSN it emitted before.
There is a second floor, and it is not electrical: constraints.rules.min_track_mm, what the person building the board asked for. Make the traces wide and easy to solder is a decision about hand assembly rather than about current, and this is where it lands — under every net, raising the default DSN rule, the per-net classes, the pour's thermal stubs and the KiCad net class together. It cannot lower a width: a rail that needs 1.4 mm keeps 1.4 mm under a 1.0 mm rule. The board's notes say how many nets it actually widened and, where the rule is wider than a pad it has to land on, that the copper will overhang its own pad — which the router will route and DRC will judge.
What 0.25 mm actually carries
About 0.87 A at a 10 °C rise on 1 oz outer copper. Until nets carried a current, that was the width of every rail on every board this pipeline built, whatever it was powering.
The router is invoked headless: java -Djava.awt.headless=true -jar vendor/freerouting-2.4.1.jar -de board.dsn -do board.ses, with a ten-minute timeout. Java is found by trying, in order, Homebrew's openjdk cellars (newest first), then java on the path — and each candidate is run, because macOS ships a /usr/bin/java stub that exists but only prints an advert. Without a working Java or the jar the board is saved unrouted and the reason is printed (no Java runtime found (freerouting needs Java 25+) / freerouting jar not found at …).
ses.py reads the session back. Freerouting writes at 100,000 units per mm while declaring 10,000, so the scale is calibrated against a placement we sent rather than trusted from either. The result is wires (net, layer, width, points) and vias, and a connectivity check: incomplete() walks the copper — every wire and via of a net is a piece, pieces are one where they touch, a pad joins every piece that enters its rectangle — and names the pins a net never reaches (GND misses J1.B1). The older coverage() asked only whether a net had any copper at all, which a net with two of three pins joined answers yes to; three boards saved under projects/ were called routed that way. The router's own count of what it left is kept beside ours (cli.route_full), and the Route pill reads the pins.
Retries. A placement that comes back with a net incomplete is not the end: workspace.route_candidates() shows the router the next-best placement, up to three, keeps the least bad — fewest incomplete nets, then fewest violations — and says which it kept and why. A clean first route costs exactly what it always did. Every attempt is written to the corpus. via_report() counts the vias the router placed against the vias the current needs — one 0.30 mm via with 25 µm of plating carries about 0.95 A, so a 2 A rail that crosses to the other side through one via is a 2 A rail through a 0.95 A conductor; the current is in the barrel, not the hole. width_report() then judges the router against its instructions — freerouting narrows a trace to squeeze it through and says nothing — comparing every wire against the width its own net was designed for. Where the net declared a current the shortfall is stated in amps: VBUS declares 2A and that copper carries 1A at a 10C rise.
The DSN and SES files are kept: the DSN in the version directory, the SES in the project's .work/ scratch directory.
Tracks, and the corners on them
freerouting does not hand back tracks. It hands back segments: on the boards in projects/, 3,838 pieces of copper are 1,156 tracks, half of every net arriving as a lone two-point wire and the rest as runs of three or four. fillet.chain() walks them back into the polylines they always were — grouped by net, layer and width, stopping at a T, where which branch continues a track is not a question the geometry answers. That moves no copper; it is the same board, said as tracks.
fillet.round_corners() then replaces each 45-degree vertex with an arc tangent to both segments, which is what KiCad's Fillet Tracks writes by hand and what Altium's router calls a rounded corner style: house style on a signal board, and load-bearing on flex (a sharp corner is a stress riser), in RF and at current. The radius asked for is three track widths, cut down to fit — no corner may take more than half of the shorter segment beside it, so two corners on a short segment cannot eat each other. On those boards 1,983 of 2,384 corners take an arc, at a median radius of 0.64mm; 401 are left sharp for want of room, and the board's notes say so.
It is a real arc everywhere it is written: an (arc (start) (mid) (end)) track in the .kicad_pcb, G75/G02/G03 circular interpolation in the Gerber, an A command in the 2D view and a tessellation in the 3D one. The film, the editor file and both pictures are one board.
The arc's centreline is always inside the copper the router already laid, and the arc's copper is not — which is the correction to what this paragraph said until 12 Sep 2026. A tangent arc lives in the triangle of (tangent point, vertex, tangent point), and _fits() checks on every corner that its centreline stays within half a width of the two segments. But a track is its centreline swept by half a width, and on the inside of the bend that sweep reaches beyond the two segments' copper: a fillet fills the re-entrant corner by (r − w/2)(1/sin α − 1) at the apex, about 48 µm on a 45° turn of a 0.25 mm track at three widths of radius. kicad-cli found it on every shipped board it was run on — nine clearance violations naming an arc out of nine, on copper that carried none before the fillet. So two things now hold instead. The pour stage judges the board with the fillet (pour.judge(fillet=)), so the copper DRC passes is the copper that ships. And every candidate arc is checked against the other-net copper near it (fillet.Obstacles: pads, the router's other tracks and vias, the board edge) at the rule plus half the width plus KiCad's own 5 µm arc tolerance; the radius is cut until the copper clears, and the corner stays sharp at the floor. fillet.paths_for() is the one call the KiCad writer, the Gerber writer, board.json, the CLI and the build's note make, so they cannot round different corners. Measured on the two boards that failed, read back through kicadpcb: violations naming an arc 12 → 0 and 11 → 0, and about half the corners beside copper at exactly the rule now take a smaller radius or none.
And the corner an arc cuts away can be the only copper touching a pad — the other direction, found 16 Sep 2026 on the A* router's copper for an ESP32-C3 dev board (projects/mcp-esp32 v001). The invariant above says the arc cannot disconnect its two segments from each other, and it cannot; but the vertex is copper too, a half-width disc, and USB_DN turned through a sharp V whose tip sat 5 µm inside its pad's edge with both legs leaving the pad. The router's board was whole, the rounded board was two islands (the arc's nearest copper 47 µm outside the pad, 34 at the floor radius), and kicad-cli was the first to say so. Obstacles.severs() is the mirror of the clearance guard: every same-net pad, via or other track the vertex touches must still be touched by the arc or the straight runs left either side of it, or the radius shrinks the way it does for added copper, down to sharp. On that board: 85 of 140 corners rounded where it had been 86, the one at the pad left sharp, DRC 0. How often the guard leaves a corner sharp elsewhere is not measured.
None of this reaches the corpus. kicad.write() rounds corners only when a caller asks it to, and only the three paths that write the board somebody receives do; every labelling and experiment path passes nothing and gets freerouting's own geometry to the micron, so no label moves because the corners got rounder.
Copper you drew yourself
A track drawn in the workbench is a constraint, not an edit to the built board — the same statement a pinned part is, about copper. It is stored in constraints.tracks and constraints.vias, and dsn.write() hands it to the router in the wiring section as (type protect). Specctra's protected wiring is copper the router may not move, and that freerouting honours it is measured rather than assumed: given a detour up and over the middle of a board that nothing would choose, it comes back in the session file verbatim.
A hand route that reaches both pads is the net finished. One that does not is a start: the router still sees the rest of the net as unconnected and completes it around what is there. workspace checks what came back rather than restating the promise — the note names how many hand-drawn tracks are on the board, and the gaps list names any that are not.
A precedent
Before the placer anneals anything, soldermask/precedent.py asks whether somebody already built this board. The corpus is indexed once by tools/annotate_boards.py into .cache/boards/annotations.jsonl: one row a board, every footprint with its refdes and the value the designer typed, the families those parts fall into (an LDO, an ESP32, a USB-C receptacle, a screw terminal), the repository, the licence the fetch recorded, and the first heading of the repository's README, which is the only sentence anywhere that says what the board is for. The lookup is on typed parts, not prose — each part of the design is paired with one part of a candidate board, identity first (a shared part number), then family, then footprint alone for the parts whose footprint is their identity — and a board is scored on how much of the design it covers and how much of it the design uses, so a product that happens to contain a breakout loses to the breakout.
A precedent past the bar (SOLDERMASK_PRECEDENT_BAR, 0.6 by default, and every IC of the design found on it) has its placement read from the DSN the corpus keeps, keyed by the design's own refs, and the anneal starts one of its six candidates from it — scaled from the precedent's outline onto this one, cooled from a quarter of the usual temperature so it is refined rather than scrambled — while the other five begin on the grid as they always have. The started candidate is rated by the same model as the rest and the router is shown them in that order, so a precedent that places badly for this build's footprints loses to the anneal instead of costing the board anything. It is a start and not a pin, and that was measured rather than decided: pinned where an RS-485 board's designer put them, this pipeline's own footprints for the same parts overlapped by 8mm², one sat 5mm off the outline, and the router left three nets after three attempts where the plain anneal routed clean in 44s. A human's pin is left out of the start and stays a pin.
All of it is said on the board: a note names the precedent, its repository and licence, how many parts it accounts for and that one anneal began from it; board.json carries the ranked precedents under precedents and marks the started one, the routing report's candidate rows carry started, and the workbench lists them under Precedents with the parts each one explains and a link to the source. SOLDERMASK_PRECEDENT=0 (or --no-precedent) keeps the listing and withholds the start; with no index file the build is exactly what it was.
What this is and is not, measured on 2026-09-08. Over 10,380 corpus boards, a board with a 95%-identical twin outside its own repository is a mirror, a demo or the same author twice, so a design will essentially never find a stranger's board to serve verbatim, and this does not try to; what it finds is a board with the same ingredients. With 400 corpus boards as the query and their own repository left out, the best other board scores 0.48 at the median (0.23 at the tenth percentile, 0.93 at the ninetieth, where the mirrors are), the bar is passed by 31%, and the top hit shares an IC family with the query 86% of the time. On the two designs it was built against, each routed by freerouting under KiCad's DRC with and without the start: for a Gemini design of an RS-485 breakout (12 parts) the corpus offered a 12-part RS-485 board at 0.61, the candidate annealed from it was rated best of the six and had the least wirelength (86mm against 112mm for the best grid anneal), routed clean first time and carried 2 DRC errors where the plain build carried 6, in 48s against 44s; for the fixture LDO breakout (8 parts) a 7-part regulator board matched at 0.65, the started candidate had the least wirelength of the six (19.1mm against 20.2mm) but was rated last by 0.005, the router was shown a grid anneal, and the board came out as the plain build did. Two boards, not a measurement of the placer; what they show is the mechanism working as designed, a precedent that helps winning on its merits and one that does not costing nothing.
A ground pour
soldermask/pour.py treats ground the way a person does: not as the net with the most pins, routed as traces like every other, but as copper poured over both outer layers that everything else is routed on top of. Every board with a ground net of two pins or more is now built this way — soldermask --no-pour (and workspace.realise(pour=False)) is how to get the old behaviour, which is what the corpus was routed with.
Three steps, in order. Ground leaves the router's problem. pour.without() hands freerouting the netlist with every ground net of two pins or more removed (pour.plane_nets); the ground pads stay on the board as copper the router keeps its clearance from, and it routes the rest. Freerouting cannot route to a pour (its issue 152, open since 2022), so the pour is not the router's to make. The pour is drawn in the KiCad board and filled there. kicad.write(..., pour=("GND",)) puts a zone for each poured net on F.Cu and B.Cu over the whole outline, with thermal reliefs and the board's own clearance; kicadcli.drc(refill_zones=True, save_board=True) has kicad-cli fill it, keeping the copper-to-edge rule, write the fill back into the file, and report which pads of the poured net the fill could not reach. Islands are stitched. A pad the signal traces wall in on its layer gets a via beside it, wherever a via fits clear of every other net's copper on both layers and of the edge, and a short trace from the pad to the via; the via reaches the other layer's pour. Fill and check again, up to three times. DRC names an island as the pad only when the fill reached none of it; a pad it reached by a thermal spoke and nothing past it -- a scrap of fill the traces walled off -- it reports as the scrap against the sheet, zone against zone, and no pad. pour.adrift() (17 Sep 2026) reads the filled pieces back off the board and joins them the way KiCad's connectivity does (a via or a through-hole pad of the net ties the pieces it sits in, a track of the net the pieces its points lie in, a surface pad the piece holding a point of its own copper -- which is where its spokes are drawn); a pad on any piece but the component with the largest one is an island for the stitcher. Before that, such a pad was pour_split, an error the stitcher could not see, and the answer was the fallback below: the LDO breakout and the ESP32 board of the MCP replay both shipped with ground routed as 26 and 117 tracks under a pour the chain called poured. A pad the stitcher cannot reach goes to the router as a net of its own (same day): pour._anchored() pairs each such pad with the nearest pad of its net the fill did reach, pour.with_ground_for() hands the router the netlist with ground cut down to those pins, the other router is asked where the first leaves a pin, and the copper goes through the same fill, stitch and DRC; it is kept only when it comes back clean, and the whole-net fallback stays behind it. pads_routed on the pour row counts them. On the 555 demo board that is 6 ground tracks of 57 where the fallback laid 55 of 170.
The verdict is a different label from the corpus's, and says so: a board is clean under the pour when the router reached every pin of every net it was given, the fill reaches every pad of the poured nets, and DRC has nothing to say about the copper. DRC is the judge of the copper, not the router's own violation count (since 15 Sep 2026): freerouting counts every clearance on the board it is handed, a footprint's own pads included, so a USB-C receptacle whose land pattern draws A1 against B12 is "4 violations" on every attempt whatever the router lays, while KiCad's DRC, which knows they are one part's pads, reports none. Read the count and such a board is never clean: the other router was asked, ground was re-routed as traces, two more placements were tried — two to six router runs a build — for a board the verdict then passed. The count still stands where DRC did not run, and it is still the corpus label. A pour in pieces counts against the copper too: an unconnected_items DRC reports between two zones is a region of ground no via or trace joins to the rest (pour_split in errors_by_kind), every pad on it reachable while the net is still two nets; nothing counted it before, and a crowded two-sided board shipped that way. Every label made this way carries pour (the nets poured) beside router, boundary_mm and layers, so a corpus that mixes the two kinds can be told apart. Two more numbers come back with it. islands_before and islands_after, with vias_added, say how much stitching the fill needed. starved counts the pads the fill reaches by one spoke where KiCad wants two, because a trace runs past the pad on the other sides: the copper is made and a 0.5 mm spoke carries more than a signal trace, so it is not a failure, but it is a weaker joint than a person would leave, and it is the placer's to avoid. What DRC reports and the verdict sets aside is counted beside it by kind (cosmetic_by_kind, said in the notes): silkscreen, courtyards, text — and, since 14 Sep 2026, footprint_pads_touching, two pads of one part on no net drawn touching, which is the vendor's land pattern for a USB-C receptacle (A1 against B12) and not copper this pipeline laid; it had been counted as a clearance violation and a solder-mask bridge on every board that carried the part.
The copper a fab is sent is the copper KiCad filled. kicad.read_fills() reads the filled_polygon blocks back out of the filled board, and the same polygons go to the Gerber writer (as RS-274X regions, the same G36/G37 the maker's mark uses), to the picture and to the page — so one program computes the fill and everything else draws it, rather than three of them each having an opinion about where the copper is. The zone stops priors.EDGE_CLEARANCE_MM inside the outline, where the router's copper stops, so the pour is not the one piece of metal sitting on the fab's minimum.
A pour is never silently missing. A board whose ground is neither routed nor poured has no ground at all, and its Gerbers would look complete — every pad, every trace. So pour.judge stands the pour down and gives ground back to the router whenever it cannot fill: no kicad-cli, a failed fill, anything. The build then behaves exactly as it did before there was a pour, and says so. If the fill worked but its copper cannot be read back, the board is saved without the pour and that is a gap on the board, not a silence.
The routability model refuses to rate a poured board. Taking ground out of the router's problem removes what is usually the highest-degree net on the board, which is a materially easier route than anything in the corpus — every example those models were fitted on has Outcome.pour of None. So routability.predict(..., poured=True) falls back to the analytic estimate and says why, exactly as it does for a four-layer board, and the note on the page reads by the rudy model (analytic: the learned models were fitted on boards whose ground was routed as traces, and this one is poured).