main
Caleb Publish 0.2.0 cbe04a0 11h ago
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142
//! Completion: what happens to a connection the first pass couldn't make, and the lanes that keep
//! fine-pitch pins from getting stuck in the first place. Three ideas, all transactional, so a
//! repair that doesn't close leaves the board exactly as it found it:
//! - Copper is the truth. What is open is read from the routed copper the way the checker reads it,
//!   so a join a fill already made isn't routed again and a join the plan forgot isn't lost.
//! - Displace, don't rip and hope. A soft search names who is in the way; those routes come up,
//!   this task goes down, and each victim must find a new place before the move is accepted.
//!   A victim keeps the parts of its route that are still legal (the splice below, from Organic
//!   Maps' AdjustRoute: search from the break to the old route and keep the old tail; VTR and
//!   nextpnr keep a route's legal part the same way). Only if that fails does it route afresh.
//! - Memory and backoff. A victim that couldn't move is banned for the next try (Freerouting's
//!   rip-up memory), and a task that fails the same way again is given up on and handed to the
//!   designer with its reason (Part 3 step 6).

use super::*;

/// Price of moving a route, nm of track: sets how far a soft search detours before it would rather
/// ask someone to move. Measured on piprobe_v13: 0.5, 1 and 2 mm land within one join of each other.
const SOFT_PRICE: f64 = 1.0 * NM;
/// Budget of a soft search and of a repair's own searches; see `pf_limit` for the small windows.
const SOFT_NODES: usize = 160_000;
const SOFT_WEIGHT: f64 = 1.5;
/// Blocker sets tried per task per round before it is left for the next round.
const REPAIR_TRIES: u32 = 4;
/// Rounds of swap moves once plain repair has stalled.
const SWAP_ROUNDS: u32 = 2;

/// One repair in progress, and how to undo it.
#[derive(Default)]
pub(super) struct Txn {
    removed: Vec<Route>,
    added: Vec<u32>,
    /// Routes laid in this repair: nothing in the same repair may take them back.
    locked: HashSet<u32>,
    /// Tasks that lost their place to a higher priority task in a swap move.
    dropped: Vec<Task>,
    banned: HashSet<(usize, usize)>,
    /// How many tasks this repair may touch, so one bad spot can't eat the whole time budget.
    fuel: i32,
    /// Each net a victim belongs to and how many pieces it was in before: a rail's tap may hang off
    /// the piece that moved, so a move is only good if no net ends up in more pieces.
    apart: HashMap<NetId, usize>,
}

#[derive(Clone, Copy)]
pub(super) struct Mode {
    /// May move higher priority data too (never pairs or Kelvin taps, 4.2).
    pub escalate: bool,
    /// May leave one lower priority victim unrouted: a sideways move that a later repair can use.
    pub swap: bool,
}

pub(super) enum Repair {
    Fixed,
    Swapped(Task),
    Failed,
}

pub(super) struct Best {
    pub score: (usize, usize, i64),
    pub routes: HashMap<u32, Route>,
    pub failed: Vec<Task>,
    pub notes: Vec<String>,
}

fn route_tie(level: Level) -> usize {
    // A hole in a higher level of the hierarchy is worse than one lower down.
    match level {
        Level::Kelvin | Level::Pair => 4,
        Level::Crystal | Level::Data => 3,
        Level::Power => 2,
        Level::PlaneFanout | Level::Other => 1,
    }
}

impl<'a> Router<'a> {
    /// A track starting at a pad's centre can break a class clearance the pads themselves are set
    /// tighter than: on benchmark B U3.1's centre line is 0.231 mm from U3.12, class 0.25 (his
    /// project excludes that DRC finding). Shifting the start across the pad, inside its copper,
    /// keeps the rule instead of bending it. Only pads whose centre fails get one.
    pub(super) fn find_pad_entries(&mut self, plan: &Plan) {
        let n = self.rules.neck;
        let half = n as f64 / 2.0;
        let pads: HashSet<usize> = plan.tasks.iter().flat_map(|t| [t.a, t.b]).collect();
        let mut found = Vec::new();
        let mut boxed = Vec::new();
        for i in pads {
            let p = &self.board.pads[i];
            let Some(layer) = p.layers.iter().copied().find(|l| self.world.route_layers.contains(l)) else { continue };
            let c = snap(p.center);
            if self.seg_clear(c, c, layer, n, p.net, NO_NET) {
                continue;
            }
            let b = p.shape.bbox();
            let (rx, ry) = (((b.2 - b.0) / 2.0 - half).max(0.0) as i64, ((b.3 - b.1) / 2.0 - half).max(0.0) as i64);
            let mut best: Option<(i64, P)> = None;
            // Steps of 0.01 mm, plus the flush offsets where the track's edge meets the pad's edge:
            // on benchmark B U3.12 (pad 0.2, neck 0.15) only the flush 0.025 mm meets the class's
            // 0.25 mm to U3.11, and it falls between two steps.
            let mut steps: Vec<i64> = (1..=50i64).map(|k| k * 10_000).chain([rx, ry]).filter(|&d| d > 0).collect();
            steps.sort_unstable();
            steps.dedup();
            for d in steps {
                for (dx, dy) in [(d, 0), (-d, 0), (0, d), (0, -d)] {
                    if dx.abs() > rx || dy.abs() > ry {
                        continue;
                    }
                    let q = snap((c.0 + dx, c.1 + dy));
                    if p.shape.dist_pt(q.into()) <= 0.0 && self.seg_clear(q, q, layer, n, p.net, NO_NET) && best.is_none() {
                        best = Some((d, q));
                    }
                }
                if best.is_some() {
                    break;
                }
            }
            match best {
                Some((_, q)) => found.push((i, q)),
                None => boxed.push(i),
            }
        }
        // No start on these pads meets the class. Where the footprint's own pads are what box one in,
        // a local rule at the pad settles it for review; otherwise only the designer can.
        let boxed: Vec<usize> = boxed.into_iter().filter(|&i| !self.propose_pad_rule(i)).collect();
        for i in boxed {
            for t in plan.tasks.iter().filter(|t| t.a == i || t.b == i) {
                self.boxed_pads.insert((t.a, t.b), i);
            }
        }
        for (i, q) in found {
            self.log.push(format!("{}: track starts {:.3} mm off the pad centre, which is closer to a neighbour than the class clearance", self.board.pad_name(i), hypot(sub(q.into(), self.board.pads[i].center.into())) / NM));
            self.pad_entry.insert(i, q);
        }
    }

    /// Length of a path that runs inside pours of other nets (those it may cross, see World).
    pub(crate) fn pour_run(&self, net: NetId, pts: &[(P, usize)]) -> f64 {
        let mut polys: Vec<(usize, &Vec<V>)> = Vec::new();
        for (z, nets) in self.world.pour_open.iter().enumerate() {
            if nets.binary_search(&net).is_ok() {
                let zone = &self.board.zones[z];
                polys.extend(zone.layers.iter().map(|&l| (l, &zone.outline)));
            }
        }
        if polys.is_empty() {
            return 0.0;
        }
        let mut run = 0.0;
        for w in pts.windows(2).filter(|w| w[0].1 == w[1].1) {
            let (a, b): (V, V) = (w[0].0.into(), w[1].0.into());
            let m = V((a.0 + b.0) / 2.0, (a.1 + b.1) / 2.0);
            if polys.iter().any(|(l, p)| *l == w[0].1 && in_poly(m, p)) {
                run += hypot(sub(a, b));
            }
        }
        run
    }

    /// The start on pad `i` that leaves the most room to its footprint's other pads, and a local
    /// rule lowering the clearance to those pads to exactly that room, never under the standing
    /// minimum. True if a track can then start there.
    fn propose_pad_rule(&mut self, i: usize) -> bool {
        let p = self.board.pads[i].clone();
        let n = self.rules.neck;
        let half = n as f64 / 2.0;
        let Some(layer) = p.layers.iter().copied().find(|l| self.world.route_layers.contains(l)) else { return false };
        let reference = self.board.footprints[p.fp].reference.clone();
        let near: Vec<usize> = self.board.footprints[p.fp].pads.iter().copied().filter(|&q| q != i && self.board.pads[q].net != p.net && self.board.pads[q].layers.contains(&layer)).collect();
        let c = snap(p.center);
        let b = p.shape.bbox();
        let (rx, ry) = (((b.2 - b.0) / 2.0 - half).max(0.0) as i64, ((b.3 - b.1) / 2.0 - half).max(0.0) as i64);
        let room = |s: P| near.iter().map(|&q| self.board.pads[q].shape.dist_pt(s.into()) - half).fold(f64::INFINITY, f64::min);
        let mut best = (room(c), c);
        for s in [(rx, 0), (-rx, 0), (0, ry), (0, -ry)].map(|d| snap((c.0 + d.0, c.1 + d.1))) {
            if p.shape.dist_pt(s.into()) <= 0.0 && room(s) > best.0 {
                best = (room(s), s);
            }
        }
        // To the micron below, so the rule's number is one a person can read and DRC agrees with.
        let min = (best.0 / 1000.0).floor() as i64 * 1000;
        if min < self.rules.floor.max(self.rules.min.clearance) {
            return false;
        }
        let needed: Vec<usize> = near.iter().copied().filter(|&q| {
            let o = &self.board.pads[q];
            o.shape.dist_pt(best.1.into()) - half < self.rules.clearance_for(p.net, o.net) as f64 && o.local_clearance <= min as f64
        }).collect();
        if needed.is_empty() {
            return false;
        }
        let net = self.board.net_name(p.net).to_string();
        let kept = self.rules.pad_rules.len();
        for q in needed {
            self.rules.pad_rules.push(PadRule { net: net.clone(), reference: reference.clone(), pad: self.board.pads[q].number.clone(), min });
        }
        self.rules.bind(self.board);
        if !self.seg_clear(best.1, best.1, layer, n, p.net, NO_NET) {
            self.rules.pad_rules.truncate(kept);
            self.rules.bind(self.board);
            return false;
        }
        self.log.push(format!("{}: no start on the pad meets the class, so a local rule at {:.3} mm is proposed", self.board.pad_name(i), to_mm(min)));
        self.pad_entry.insert(i, best.1);
        true
    }

    /// A way off a big pour for pad `i`: the via site within a short stub that leaves the most room to
    /// its footprint's other pads, and local rules lowering the clearance to just that room, never
    /// under the standing minimum. True if it added a rule to try. His own exit at J1 on benchmark B
    /// sits inside USB's 0.4 mm class in the same way, with those DRC findings excluded by hand.
    pub(super) fn propose_exit_rule(&mut self, i: usize) -> bool {
        if !self.pour_exit_pads.contains(&i) {
            return false;
        }
        let p = self.board.pads[i].clone();
        let net = p.net;
        let layer = p.layers[0];
        let (via_d, _) = self.rules.via_for(net);
        let vr = via_d as f64 / 2.0;
        let half = self.rules.neck as f64 / 2.0;
        let own = vr + self.rules.pad_via_gap as f64;
        let reference = self.board.footprints[p.fp].reference.clone();
        let near: Vec<usize> = self.board.footprints[p.fp].pads.iter().copied().filter(|&q| q != i && self.board.pads[q].net != net && self.board.pads[q].layers.contains(&layer)).collect();
        let c = snap(p.center);
        let room_at = |s: P| -> f64 {
            near.iter().map(|&q| {
                let o = &self.board.pads[q].shape;
                (o.dist_pt(s.into()) - vr).min(o.dist_seg(c.into(), s.into()) - half)
            }).fold(f64::INFINITY, f64::min)
        };
        let mut best: Option<(f64, P)> = None;
        for dir in 0..8usize {
            let (dx, dy) = DIRS[dir];
            for k in 1..=(self.pour_limit / 50_000.0) as i64 {
                let d = k * 50_000;
                let t = if dx != 0 && dy != 0 { (d as f64 / SQRT2) as i64 } else { d };
                let s = snap((c.0 + dx * t, c.1 + dy * t));
                if p.shape.dist_pt(s.into()) < own || self.world.mask.via_hit(net, s.into(), vr).is_some() {
                    continue;
                }
                let room = room_at(s);
                if best.map(|b| room > b.0).unwrap_or(true) {
                    best = Some((room, s));
                }
            }
        }
        let Some((room, _)) = best else { return false };
        let min = (room / 1000.0).floor() as i64 * 1000;
        if min < self.rules.floor.max(self.rules.min.clearance) {
            return false;
        }
        let name = self.board.net_name(net).to_string();
        let mut added = false;
        for &q in &near {
            let o = &self.board.pads[q];
            let number = o.number.clone();
            if (min as f64) >= self.rules.clearance_for(net, o.net) as f64 || o.local_clearance > min as f64 {
                continue;
            }
            if self.rules.pad_rules.iter().any(|r| r.net == name && r.reference == reference && r.pad == number && r.min <= min) {
                continue;
            }
            self.rules.pad_rules.retain(|r| !(r.net == name && r.reference == reference && r.pad == number));
            self.rules.pad_rules.push(PadRule { net: name.clone(), reference: reference.clone(), pad: number, min });
            added = true;
        }
        if added {
            self.rules.bind(self.board);
            self.log.push(format!("{}: no way off the pour under the class, so local rules at {:.3} mm are proposed", self.board.pad_name(i), to_mm(min)));
        }
        added
    }

    /// A proposal stands only where a track really uses it, and then the designer hears about it.
    pub(super) fn settle_pad_rules(&mut self) {
        let mut used = Vec::new();
        for rule in std::mem::take(&mut self.rules.pad_rules) {
            let Some(net) = self.board.net_ids.get(&rule.net).copied() else { continue };
            // A number can name several pads: a connector's shell pads are all SH.
            let vr = self.rules.via_for(net).0 as f64 / 2.0;
            let mut tightest: Option<(f64, f64)> = None;
            for q in self.board.pads.iter().filter(|q| q.number == rule.pad && self.board.footprints[q.fp].reference == rule.reference) {
                let class = self.rules.clearance_for(net, q.net) as f64;
                let track = self.routes.values().filter(|r| r.task.net == net).flat_map(|r| r.segs.iter()).filter(|s| q.layers.contains(&s.layer)).map(|s| q.shape.dist_seg(s.a.into(), s.b.into()) - s.width as f64 / 2.0).fold(f64::INFINITY, f64::min);
                let closest = self.routes.values().filter(|r| r.task.net == net).flat_map(|r| r.vias.iter()).map(|v| q.shape.dist_pt((*v).into()) - vr).fold(track, f64::min);
                if closest < class - DRC_TOL && tightest.map(|t| closest < t.0).unwrap_or(true) {
                    tightest = Some((closest, class));
                }
            }
            if let Some((closest, class)) = tightest {
                self.notes.push(format!("for the designer: {} passes {} {} at {:.3} mm under a proposed local rule (class {:.3} mm): no track fits its pad otherwise; review the rule in the output's .kicad_dru", rule.net, rule.reference, rule.pad, closest / NM, class / NM));
                used.push(rule);
            }
        }
        self.rules.pad_rules = used;
        self.rules.bind(self.board);
    }

    /// `pour_run` of a laid route, segment by segment: a route's segments need not join end to end.
    pub(crate) fn seg_pour_run(&self, r: &Route) -> f64 {
        r.segs.iter().map(|s| self.pour_run(r.task.net, &[(s.a, s.layer), (s.b, s.layer)])).sum()
    }

    /// The pours this board's copper would split on a refill, by net name: (net, extra pieces,
    /// cutting nets). Judged from the written board, the same way the checker judges it.
    fn pour_splits(&self) -> Vec<(String, usize, Vec<NetId>)> {
        // In id order: a hash map's order changes on every start of the program (clock.rs).
        let mut routes: Vec<&Route> = self.routes.values().collect();
        routes.sort_by_key(|r| r.id);
        let text = crate::write::render(self.board, &routes, &self.rules);
        let Ok(written) = Board::parse(text) else { return Vec::new() };
        let mut rules = self.rules.clone();
        rules.bind(&written);
        let world = build_world(&written, &rules);
        crate::pour::splits(&written, &rules, &world)
            .into_iter()
            .map(|s| (written.net_name(s.net).to_string(), s.extra, s.cutters.iter().filter_map(|&c| self.board.net_ids.get(written.net_name(c)).copied()).collect()))
            .collect()
    }

    /// Ground stitching, and the same for any poured net: where a refill would leave a piece of a
    /// pour apart from the rest of its net, a via ties it to the net's copper on another layer.
    /// The boards on the wiki do this everywhere (the designer's and another designer's have 100 to 300 vias where the
    /// router laid 20 to 60), and without it KiCad reports the pieces unconnected: on four of the
    /// first eight wiki boards the router called every join made while KiCad found 10 to 16 open
    /// (2026-11-10). Judged from the written board; a via stays only where one is legal and the
    /// net has a pour or plane on another layer there.
    pub(super) fn stitch_pours(&mut self) {
        let t0 = Instant::now();
        let (mut added, mut left) = (0usize, 0usize);
        let mut tried: HashSet<P> = HashSet::new();
        let mut tried_pairs: HashSet<(usize, usize)> = HashSet::new();
        let mut failed_pieces: HashSet<usize> = HashSet::new();
        let mut tracks = 0usize;
        let octile_p = |a: P, b: P| -> f64 {
            let (dx, dy) = ((a.0 - b.0).abs() as f64, (a.1 - b.1).abs() as f64);
            dx.max(dy) + (SQRT2 - 1.0) * dx.min(dy)
        };
        for _round in 0..60 {
            if self.over_time() {
                break;
            }
            let mut routes: Vec<&Route> = self.routes.values().collect();
            routes.sort_by_key(|r| r.id);
            let text = crate::write::render(self.board, &routes, &self.rules);
            let Ok(written) = Board::parse(text) else { return };
            let mut rules = self.rules.clone();
            rules.bind(&written);
            let world = build_world(&written, &rules);
            let nets: std::collections::BTreeSet<NetId> = written.zones.iter().filter(|z| z.net != NO_NET && !z.keepout_tracks && z.outline.len() >= 3).map(|z| z.net).collect();
            let (mut placed, mut loose) = (0usize, 0usize);
            for wnet in nets {
                let Some(&net) = self.board.net_ids.get(written.net_name(wnet)) else { continue };
                // A run asked for a few nets (a fixture) mends only those.
                if self.only.as_ref().map_or(false, |o| !o.contains(&net)) {
                    continue;
                }
                let Some(pad) = self.board.pads.iter().position(|p| p.net == net && !p.layers.is_empty()) else { continue };
                for (layer, cells, island_pads) in crate::pour::loose_islands(&written, &rules, &world, wnet) {
                    loose += 1;
                    let mut done = false;
                    // Somewhere to go: the net's pour or plane on another layer over this point.
                    let other = |c: V| self.board.zones.iter().any(|z| z.net == net && !z.keepout_tracks && z.layers.iter().any(|&l| l != layer) && in_poly(c, &z.outline));
                    let profile = Profile { width: 0, neck: 0, clearance: self.rules.clearance_for(net, net), target_gap: 0, max_vias: 0, margin: 0.0, layer_bias: None };
                    let q = Query { net, also: None, sources: &[], targets: &[], profile, rules: &self.rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
                    for c in cells {
                        let at = crate::search::snap(to_p(c));
                        if !tried.insert(at) || !other(c) {
                            continue;
                        }
                        let (mut b, mut sc) = (Vec::new(), 0.0);
                        if self.world.via_ok(at, &q, None, &mut b, &mut sc) {
                            let id = self.fresh_id();
                            let task = Task { net, level: Level::PlaneFanout, a: pad, b: pad, main: false, zone: None };
                            self.place(id, task, Vec::new(), vec![at]);
                            placed += 1;
                            done = true;
                            break;
                        }
                    }
                    // No room on the piece itself, but the net's other layer lies under one of its pads:
                    // a via beside that pad, on a short stub, as a plane pad gets (4.2). Cheaper than a
                    // track and the way he ties such a pad (a motor-controller board, Q302.2 over the B.Cu plane).
                    if !done {
                        for &ip in &island_pads {
                            let at = self.board.pads[ip].center;
                            if !other(at.into()) || !tried.insert(at) {
                                continue;
                            }
                            let task = Task { net, level: Level::PlaneFanout, a: ip, b: ip, main: false, zone: None };
                            if self.fan_out_pad(ip, &task) {
                                placed += 1;
                                done = true;
                                break;
                            }
                        }
                    }
                    // No copper of the net on another layer to drop to, or no room for the via: a track
                    // from a pad on this piece to the nearest pad of the net off it.
                    // One try per piece: the join already aims at every piece off this one, so a second
                    // try from another of its pads fails the same way (it used up stitching's time).
                    let piece_key = island_pads.iter().copied().min();
                    if !done && !island_pads.is_empty() && !piece_key.map_or(false, |k| failed_pieces.contains(&k)) {
                        let c = |i: usize| self.board.pads[i].center;
                        let mut best: Option<(f64, usize, usize)> = None;
                        for &ip in &island_pads {
                            for (o, p) in self.board.pads.iter().enumerate() {
                                if p.net != net || p.layers.is_empty() || island_pads.contains(&o) {
                                    continue;
                                }
                                let d = octile_p(c(ip), c(o));
                                if best.map_or(true, |b| d < b.0) && !tried_pairs.contains(&(ip, o)) {
                                    best = Some((d, ip, o));
                                }
                            }
                        }
                        if let Some((_, ip, o)) = best {
                            tried_pairs.insert((ip, o));
                            let off: Vec<usize> = self.board.pads.iter().enumerate().filter(|(i, p)| p.net == net && !p.layers.is_empty() && !island_pads.contains(i)).map(|(i, _)| i).collect();
                            if self.join_by_track(net, ip, o, &off) {
                                placed += 1;
                                tracks += 1;
                                done = true;
                            } else if let Some(k) = piece_key {
                                failed_pieces.insert(k);
                            }
                        }
                    }
                    // Still loose: a via beside a pad on it, on a short stub, as a plane pad gets (4.2).
                    if !done {
                        for ip in island_pads {
                            if !tried.insert(self.board.pads[ip].center) {
                                continue;
                            }
                            let task = Task { net, level: Level::PlaneFanout, a: ip, b: ip, main: false, zone: None };
                            if self.fan_out_pad(ip, &task) {
                                placed += 1;
                                break;
                            }
                        }
                    }
                }
            }
            added += placed;
            left = loose - placed.min(loose);
            if placed == 0 {
                break;
            }
        }
        if added > 0 || left > 0 {
            self.log.push(format!("stitching: {} vias tie pour pieces to their net on another layer, {tracks} tracks join pads a pour leaves apart; {left} pieces had no place for either, {:.1} s", added - tracks, t0.elapsed().as_secs_f64()));
        }
    }

    /// A track from the copper of pad `a`'s piece to the nearest copper of the pads `to`, searched
    /// without the net's pour as a target, since the pour is what failed to join them. Both ends are
    /// whole pieces (pads and their tracks), so the new track never runs along one already there.
    fn join_by_track(&mut self, net: NetId, a: usize, b: usize, to: &[usize]) -> bool {
        let task = Task { net, level: Level::Other, a, b, main: false, zone: None };
        let (width, neck) = self.widths_for(&task);
        let clearance = self.rules.clearance_for(net, net);
        let comps = self.components_with(net, false);
        let (mut sources, mut targets) = (Vec::new(), Vec::new());
        for c in &comps {
            if c.pads.contains(&a) {
                self.comp_terminals(c, 0.0, &mut sources);
            } else if c.pads.iter().any(|p| to.contains(p)) {
                self.comp_terminals(c, 0.0, &mut targets);
            }
        }
        if sources.is_empty() || targets.is_empty() {
            return false;
        }
        // A mend, not a first route: a join that is not found quickly is left for the designer's note.
        // At the full node limit a few hopeless joins used all of stitching's time on a motor-controller board.
        let mut rules = self.rules.clone();
        rules.node_limit = rules.node_limit.min(60_000);
        // A search leaves from pads and may land on a track, never the reverse. So both ways are
        // tried and the shorter kept: the side that already has a track toward the other is then
        // the one landed on, and nothing is laid twice.
        let len = |pts: &[(P, usize)]| -> f64 { pts.windows(2).map(|w| hypot(sub(w[0].0.into(), w[1].0.into()))).sum() };
        for (max_vias, margin) in [(0u32, 3.0), (2, 6.0), (2, 15.0)] {
            let pf = Profile { width, neck, clearance, target_gap: clearance, max_vias, margin: margin * NM, layer_bias: None };
            let mut best: Option<(f64, Vec<(P, usize)>)> = None;
            for (from, to) in [(&sources, &targets), (&targets, &sources)] {
                let q = Query { net, also: None, sources: from, targets: to, profile: pf, rules: &rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
                self.searches += 1;
                let Some(path) = search(&self.world, &q) else { continue };
                self.nodes += path.nodes as u64;
                let pts = self.finish_path(&path, net, NO_NET, neck);
                let l = len(&pts);
                if best.as_ref().map_or(true, |b| l < b.0) {
                    best = Some((l, pts));
                }
            }
            if let Some((_, pts)) = best {
                self.commit(&task, &pts, width, neck, NO_NET);
                return true;
            }
        }
        false
    }

    /// Takes the nets that cut a pour off it. His way (benchmark B USB connector): a short stub off
    /// the pad and straight to the other layer. One net at a time first, then all of them together,
    /// since two neighbours can each block the other's way down. A move stands only if the refill
    /// model says the pour is better for it and no join opened; otherwise the old copper goes back.
    pub(super) fn heal_pours(&mut self) {
        self.grant_time(60.0);
        for _ in 0..8 {
            let splits = self.pour_splits();
            let total: usize = splits.iter().map(|s| s.1).sum();
            if total == 0 {
                break;
            }
            let cutters: Vec<NetId> = splits.iter().flat_map(|s| s.2.iter().copied()).collect();
            let mut sets: Vec<Vec<NetId>> = cutters.iter().map(|&c| vec![c]).collect();
            if cutters.len() > 1 {
                sets.push(cutters.clone());
            }
            // A net alone may not be the cut: a pour can be blocked on both sides of a connector by
            // different nets, and only moving them together opens it. So every net leaving a big pour.
            let mut leaving: Vec<NetId> = self.pour_exit.iter().copied().filter(|&n| !looks_ground(self.board.net_name(n))).collect();
            leaving.sort_unstable();
            if leaving.len() > 1 {
                sets.push(leaving);
            }
            if !sets.iter().any(|set| self.move_off_pour(set, total)) {
                break;
            }
        }
        for (net, extra, cutters) in self.pour_splits() {
            let names: Vec<&str> = cutters.iter().map(|&c| self.board.net_name(c)).collect();
            self.notes.push(format!("for the designer: the {net} pour refills into {extra} extra separate pieces, cut by {}: move them or bridge the pieces", names.join(", ")));
        }
    }

    fn move_off_pour(&mut self, nets: &[NetId], total: usize) -> bool {
        let names = nets.iter().map(|&c| self.board.net_name(c).to_string()).collect::<Vec<_>>().join(", ");
        // Even a short link can cut a pour: D+ tying a USB-C receptacle's two rows together across the
        // gap between them. So anything more than a stub off a pad is up for moving, and may not come
        // back longer than one (reference J1: each pad drops to the run by its own via).
        let ids: Vec<u32> = self.routes.values().filter(|r| nets.contains(&r.task.net) && self.seg_pour_run(r) > STUB_RUN).map(|r| r.id).collect();
        if ids.is_empty() {
            self.log.push(format!("pours: {names}: nothing more than a stub in the pour to move"));
            return false;
        }
        self.pour_limit = STUB_RUN;
        let open: usize = nets.iter().map(|&c| self.open_pieces(c)).sum();
        let saved: Vec<Route> = ids.iter().filter_map(|&i| self.unplace(i)).collect();
        let others: HashSet<u32> = self.routes.keys().copied().collect();
        self.pour_strict.extend(nets.iter().copied());
        let mut failed = 0;
        for r in &saved {
            if !self.try_route(&r.task) {
                failed += 1;
            }
        }
        for c in nets {
            self.pour_strict.remove(c);
        }
        self.pour_limit = POUR_RUN;
        let now_open: usize = nets.iter().map(|&c| self.open_pieces(c)).sum();
        let after: usize = if failed == 0 && now_open <= open { self.pour_splits().iter().map(|s| s.1).sum() } else { total };
        if after < total {
            self.log.push(format!("pours: moved {names} off the pour, extra pieces {total} to {after}"));
            return true;
        }
        let new: Vec<u32> = self.routes.keys().copied().filter(|i| !others.contains(i)).collect();
        for i in new {
            self.unplace(i);
        }
        for r in saved {
            self.place(r.id, r.task, r.segs, r.vias);
        }
        self.log.push(format!("pours: {names}: put back ({} of {} routes found no way off the pour, joins open {open} to {now_open}, extra pieces {total} to {after})", failed, ids.len()));
        false
    }

    /// Hold a pin's way out, straight off the pad, until its route is laid, on any part whose pitch
    /// makes the straight line that pin's only way out. That is decided from geometry, not from the
    /// part: a track on a neighbour's line can't get past a via on this one when the pitch is under
    /// the via's radius + clearance + half the track. RP-family parts qualify at 0.4 mm, and so does
    /// anything else that tight; the fine-pitch rule in the Troubleshooting doc ("parallel at pin
    /// pitch before calling it blocked") applies to them all, while 4.7's supply rings don't and
    /// aren't here. An earlier route then can't cut across the row and a GND via can't stand in the
    /// lane, and bus lines stagger their turns by themselves. Pins that stub inward to their own fill
    /// have no lane.
    pub(super) fn lay_lanes(&mut self, plan: &Plan) {
        // A new order of battle on a restart: lanes from the last run come off first.
        let old: Vec<usize> = self.lanes.keys().copied().collect();
        for pad in old {
            self.lane_off(pad);
        }
        self.lanes.clear();
        if self.lane_len <= 0.0 {
            return;
        }
        let inward: HashSet<usize> = plan.tasks.iter().filter(|t| t.zone.is_some_and(|z| z != RAIL_TAP)).map(|t| t.a).collect();
        // Where each pin's route is headed, for which way its lane's neighbours should stair.
        let mut goes_to: HashMap<usize, V> = HashMap::new();
        for t in plan.tasks.iter().filter(|t| t.a != t.b) {
            goes_to.entry(t.a).or_insert(self.board.pads[t.b].center.into());
            goes_to.entry(t.b).or_insert(self.board.pads[t.a].center.into());
        }
        let working: HashSet<usize> = plan.tasks.iter().flat_map(|t| [t.a, t.b]).collect();
        let r = self.rules.neck as f64 / 2.0;
        let stair = 0.06 * NM;
        let mut lanes = Vec::new();
        for f in &self.board.footprints {
            if f.min_pitch >= (self.rules.via as f64 / 2.0) + self.rules.clearance as f64 + self.rules.neck as f64 / 2.0 {
                continue;
            }
            let n = f.pads.len().max(1) as f64;
            let cx: f64 = f.pads.iter().map(|&p| self.board.pads[p].center.0 as f64).sum::<f64>() / n;
            let cy: f64 = f.pads.iter().map(|&p| self.board.pads[p].center.1 as f64).sum::<f64>() / n;
            // One row per side: (runs along y, outward sign) -> (position along the row, pad, tip).
            let mut rows: HashMap<(bool, bool), Vec<(f64, usize, V, usize)>> = HashMap::new();
            for &i in &f.pads {
                let p = &self.board.pads[i];
                if !working.contains(&i) || inward.contains(&i) {
                    continue;
                }
                let Some(layer) = p.layers.iter().copied().find(|l| self.world.route_layers.contains(l)) else { continue };
                let b = p.shape.bbox();
                let (w, h) = (b.2 - b.0, b.3 - b.1);
                let c: V = p.center.into();
                // The pad's long axis points the way out; a square pad has no such way.
                if w > h * 1.5 {
                    let out = c.0 >= cx;
                    rows.entry((true, out)).or_default().push((c.1, i, V(if out { b.2 } else { b.0 }, c.1), layer));
                } else if h > w * 1.5 {
                    let out = c.1 >= cy;
                    rows.entry((false, out)).or_default().push((c.0, i, V(c.0, if out { b.3 } else { b.1 }), layer));
                }
            }
            for ((along_y, out), mut row) in rows {
                row.sort_by(|a, b| a.0.total_cmp(&b.0));
                let turn = |i: usize, at: f64| -> i32 {
                    let Some(t) = goes_to.get(&i) else { return 0 };
                    let d = if along_y { t.1 } else { t.0 } - at;
                    if d.abs() < f.min_pitch { 0 } else if d > 0.0 { 1 } else { -1 }
                };
                // Neighbours turning the same way at 45 degrees need more than the pin pitch between
                // them, so their turns stair by a little each (reference QSPI bus: "each bends further
                // out"). The pin on the inside of the turn gets the shortest lane, or the bus closes.
                let mut k = 0;
                while k < row.len() {
                    let s = turn(row[k].1, row[k].0);
                    let mut e = k + 1;
                    while e < row.len() && s != 0 && turn(row[e].1, row[e].0) == s && row[e].0 - row[e - 1].0 < f.min_pitch * 1.5 {
                        e += 1;
                    }
                    for j in k..e {
                        let (_, i, tip, layer) = row[j];
                        let steps = if s > 0 { e - 1 - j } else { j - k } as f64;
                        let len = if e - k == 1 { self.lane_len } else { (self.lane_len * 0.5 + stair * steps).min(self.lane_len * 2.0) };
                        let sign = if out { 1.0 } else { -1.0 };
                        // A lane stops short of fixed copper in its way: a part placed right off
                        // the pin must stay reachable, its own pad most of all.
                        let net = self.board.pads[i].net;
                        let mut len = len;
                        let mut to = tip;
                        while len >= 0.1 * NM {
                            to = if along_y { V(tip.0 + sign * len, tip.1) } else { V(tip.0, tip.1 + sign * len) };
                            if self.seg_clear(to_p(tip), to_p(to), layer, self.rules.neck, net, NO_NET) {
                                break;
                            }
                            len -= 0.05 * NM;
                        }
                        if len >= 0.1 * NM {
                            lanes.push((i, layer, Shape::Capsule { a: tip, b: to, r }));
                        }
                    }
                    k = e;
                }
            }
        }
        lanes.sort_by_key(|l| l.0);
        for (i, layer, shape) in lanes {
            self.lanes.insert(i, (layer, shape, None));
            self.lane_on(i);
        }
    }

    pub(super) fn lane_off(&mut self, pad: usize) {
        if let Some((layer, _, h)) = self.lanes.get_mut(&pad) {
            if let Some(id) = h.take() {
                self.world.layers[*layer].remove(id);
            }
        }
    }

    pub(super) fn lane_on(&mut self, pad: usize) {
        if !self.lanes.contains_key(&pad) || self.routes.values().any(|r| r.task.a == pad || r.task.b == pad) {
            return;
        }
        let net = self.board.pads[pad].net;
        let (layer, shape, h) = self.lanes.get_mut(&pad).unwrap();
        if h.is_none() {
            *h = Some(self.world.layers[*layer].add(shape.clone(), net, Owner::Lane(pad as u32)));
        }
    }

    fn pieces_of(&self, net: NetId) -> Vec<(NetId, conn::Piece)> {
        let mut extra = Vec::new();
        for r in self.routes.values().filter(|r| r.task.net == net) {
            for s in &r.segs {
                extra.push((net, conn::Piece::Seg { layer: s.layer, a: s.a.into(), b: s.b.into(), r: s.width as f64 / 2.0 }));
            }
            for v in &r.vias {
                extra.push((net, conn::Piece::Via { at: (*v).into(), r: self.rules.via as f64 / 2.0 }));
            }
        }
        extra
    }

    /// A net's islands as the checker sees them: pads, routed copper and fills.
    pub(crate) fn islands(&self, net: NetId) -> Vec<Vec<usize>> {
        let Some((_, pads)) = self.net_info.get(&net) else { return Vec::new() };
        conn::net_groups(self.board, net, &self.pieces_of(net))
            .into_iter()
            .map(|g| g.into_iter().filter(|p| pads.contains(p)).collect::<Vec<_>>())
            .filter(|g: &Vec<usize>| !g.is_empty())
            .collect()
    }

    /// Does the copper already join this task's two pads? Nets of two pads can't be joined by
    /// anything but their own route, so they skip the work.
    pub(super) fn joined(&self, task: &Task) -> bool {
        if task.a == task.b || task.zone.is_some_and(|z| z != RAIL_TAP) {
            return false;
        }
        let Some((_, pads)) = self.net_info.get(&task.net) else { return false };
        if pads.len() < 3 {
            return false;
        }
        self.islands(task.net).iter().any(|g| g.contains(&task.a) && g.contains(&task.b))
    }

    /// What is really open: the failed tasks that still join two islands, plus the joins nothing
    /// asked for. Plane fan-outs keep their own record, a pad either has its via or it hasn't.
    pub(super) fn open_work(&self, failed: Vec<Task>) -> Vec<Task> {
        let mut out: Vec<Task> = failed.iter().filter(|t| t.a == t.b && t.zone.is_none() && t.level == Level::PlaneFanout).cloned().collect();
        let planes = self.board.plane_nets();
        let mut nets: Vec<NetId> = self.net_info.keys().copied().collect();
        nets.sort();
        for net in nets {
            if self.only.as_ref().map(|o| !o.contains(&net)).unwrap_or(false) {
                continue;
            }
            let (level, _) = self.net_info[&net];
            if level == Level::PlaneFanout && planes.contains_key(&net) {
                continue;
            }
            // A rail joins as a rail: whatever is apart taps the rest, by the bus and its stubs,
            // and a fill nothing feeds counts too, since an island is never acceptable (4.3).
            if level == Level::Power {
                out.extend(self.rail_work(net));
                continue;
            }
            let live = self.islands(net);
            if live.len() < 2 {
                continue;
            }
            let mut uf = conn::Uf::new(live.len());
            let at = |p: usize| live.iter().position(|g| g.contains(&p));
            for t in failed.iter().filter(|t| t.net == net && t.a != t.b && t.zone.is_none()) {
                if let (Some(x), Some(y)) = (at(t.a), at(t.b)) {
                    if uf.find(x as u32) != uf.find(y as u32) {
                        uf.union(x as u32, y as u32);
                        out.push(t.clone());
                    }
                }
            }
            // Whatever is still apart joins at its nearest pads (Prim over the islands).
            loop {
                let mut best: Option<(f64, usize, usize, usize, usize)> = None;
                for i in 0..live.len() {
                    for j in i + 1..live.len() {
                        if uf.find(i as u32) == uf.find(j as u32) {
                            continue;
                        }
                        for &a in &live[i] {
                            for &b in &live[j] {
                                let d = hypot(sub(self.board.pads[a].center.into(), self.board.pads[b].center.into()));
                                if best.map(|x| d < x.0).unwrap_or(true) {
                                    best = Some((d, i, j, a, b));
                                }
                            }
                        }
                    }
                }
                let Some((_, i, j, a, b)) = best else { break };
                uf.union(i as u32, j as u32);
                out.push(Task { net, level, a, b, main: false, zone: None });
            }
        }
        // A fill nothing feeds is open too, though no pad says so (4.3: islands, never).
        out.extend(self.unfed_fills());
        out.sort_by_key(|t| t.level);
        out
    }

    /// One tap per piece of a rail still apart: from the pad of each smaller piece nearest the
    /// biggest, then from the nearest pad to each fill with no feed.
    fn rail_work(&self, net: NetId) -> Vec<Task> {
        let comps = self.components(net);
        let mk = |p: usize| Task { net, level: Level::Power, a: p, b: p, main: false, zone: Some(RAIL_TAP) };
        let with_pads: Vec<&crate::post::Comp> = comps.iter().filter(|c| !c.pads.is_empty()).collect();
        let Some(big) = with_pads.iter().max_by_key(|c| c.pads.len() + c.segs.len()) else { return Vec::new() };
        let dist = |a: usize, b: usize| hypot(sub(self.board.pads[a].center.into(), self.board.pads[b].center.into()));
        let mut out = Vec::new();
        for c in with_pads.iter().filter(|c| !std::ptr::eq(**c, *big)) {
            let near = |p: usize| big.pads.iter().map(|&q| dist(p, q)).fold(f64::INFINITY, f64::min);
            if let Some(&p) = c.pads.iter().min_by(|&&x, &&y| near(x).total_cmp(&near(y))) {
                out.push(mk(p));
            }
        }
        for c in comps.iter().filter(|c| c.pads.is_empty() && c.zones.iter().any(|&z| self.world.route_layers.contains(&self.world.zones[z].1))) {
            let z = c.zones[0];
            let poly = &self.world.zones[z].2;
            if poly.is_empty() {
                continue;
            }
            let m = V(poly.iter().map(|p| p.0).sum::<f64>() / poly.len() as f64, poly.iter().map(|p| p.1).sum::<f64>() / poly.len() as f64);
            if let Some(&p) = big.pads.iter().min_by(|&&x, &&y| hypot(sub(self.board.pads[x].center.into(), m)).total_cmp(&hypot(sub(self.board.pads[y].center.into(), m)))) {
                out.push(mk(p));
            }
        }
        out
    }

    /// How many pieces a net is in, for telling whether a move took it apart.
    fn pieces_apart(&self, net: NetId) -> usize {
        match self.net_info.get(&net) {
            Some((Level::Power, _)) => self.rail_work(net).len() + 1,
            Some((Level::PlaneFanout, _)) => 1,
            _ => self.islands(net).len(),
        }
    }

    /// Fullest first, then the fewest holes high in the hierarchy, then the cleanest copper.
    pub(super) fn score(&self, failed: &[Task]) -> (usize, usize, i64) {
        let holes: usize = failed.iter().map(|t| route_tie(t.level)).sum();
        let mut cost = 0.0;
        for r in self.routes.values() {
            for s in &r.segs {
                cost += hypot(sub(s.a.into(), s.b.into()));
            }
            cost += r.vias.len() as f64 * self.rules.cost_via;
        }
        // Of two boards equally full, the one with the bus plan: its copper may be longer, but the plain
        // go exists to close holes, not to win on length (4.2 weighs order against distance by hand).
        let plain = if self.plain_go { i64::MAX / 4 } else { 0 };
        (failed.len(), holes, cost as i64 + plain)
    }

    pub(super) fn keep_if_best(&self, failed: &[Task], best: &mut Option<Best>) {
        let score = self.score(failed);
        if best.as_ref().map(|b| score < b.score).unwrap_or(true) {
            *best = Some(Best { score, routes: self.routes.clone(), failed: failed.to_vec(), notes: self.notes.clone() });
        }
    }

    fn undo(&mut self, txn: Txn) {
        for id in txn.added {
            self.rip(id);
        }
        for r in txn.removed.into_iter().rev() {
            self.place(r.id, r.task, r.segs, r.vias);
        }
    }

    /// Who may be moved for this task, at what price. GND vias may always move, they are the
    /// lowest priority copper (4.4). Pairs and Kelvin never move for anything lower (4.2).
    fn prices_for(&self, task: &Task, mode: Mode, txn: &Txn) -> HashMap<u32, f64> {
        let me = (task.a, task.b);
        self.routes
            .values()
            .map(|r| {
                let them = (r.task.a, r.task.b);
                let protected = matches!(r.task.level, Level::Kelvin | Level::Pair) || (r.segs.is_empty() && r.task.a == r.task.b);
                let via = r.task.level == Level::PlaneFanout;
                let lower = r.task.level >= task.level || via;
                let times = self.memory.get(&(me, them)).copied().unwrap_or(0);
                let rc = self.rip_count.get(&them).copied().unwrap_or(0) as f64;
                let closed = protected || (!lower && !mode.escalate) || times >= 6 || txn.locked.contains(&r.id) || txn.banned.contains(&them);
                let base = if via { 0.3 * NM } else { SOFT_PRICE };
                (r.id, if closed { f64::INFINITY } else { base * (1.0 + 0.25 * rc) })
            })
            .collect()
    }

    /// A soft search with a tight budget, a coarser lattice and a greedier heuristic. Free copper
    /// is flooded first at the usual weights, so an unbudgeted exact one spends everything before
    /// paying to cross anyone: measured on piprobe_v13 as 800k expansions, 8 s and no answer for a
    /// pin 0.4 mm from its way out.
    fn soft_path(&mut self, task: &Task, mode: Mode, txn: &Txn) -> Option<(Path, Profile)> {
        let prices = self.prices_for(task, mode, txn);
        let price = |c: u32| prices.get(&c).copied().unwrap_or(f64::INFINITY);
        let sources = [self.pad_terminal(task.a)];
        let targets = self.targets_of(task);
        let (_, n) = self.widths_for(task);
        let mut rules = self.rules.clone();
        rules.node_limit = SOFT_NODES * if self.deep { 6 } else { 1 };
        rules.search_weight = SOFT_WEIGHT;
        // Non-priority nets and rails go for the via first: a rail's way is along its bus.
        let bias = self.bus_of(task);
        let ladder: &[(u32, f64)] = if task.level == Level::Other || bias.is_some() { &[(2, 3.0), (2, 10.0), (4, 40.0)] } else { &[(0, 3.0), (2, 3.0), (2, 10.0), (4, 40.0)] };
        for &(vias, margin) in ladder {
            let pf = Profile { width: n, neck: n, clearance: self.rules.clearance, target_gap: self.rules.clearance, max_vias: vias, margin: margin * NM, layer_bias: bias };
            let q = Query { net: task.net, also: None, sources: &sources, targets: &targets, profile: pf, rules: &rules, soft: Some(&price), via_in: None, history: Some(&self.history), free: &[], arrive: None };
            self.searches += 1;
            if let Some(path) = search(&self.world, &q) {
                self.nodes += path.nodes as u64;
                return Some((path, pf));
            }
            if self.trace {
                eprintln!("    soft {} {} -> {} vias {} margin {}: {}", self.board.net_name(task.net), self.board.pad_name(task.a), self.board.pad_name(task.b), vias, margin, if RAN_OUT.with(|c| c.get()) { "ran out" } else { "walled in" });
            }
        }
        None
    }

    /// A legal route for this task with a budget fit for a repair, not a first attempt. Power goes to
    /// the bus first, like any other rail task (4.2): a repair is no reason to wrap a chip on the pad
    /// layer. It may then join anywhere on the other island, a pad, a via or a bend (a branch, not a
    /// new run).
    pub(super) fn route_hard(&mut self, task: &Task) -> Option<u32> {
        if task.level == Level::Power && task.zone.map(|z| z == RAIL_TAP).unwrap_or(true) {
            let rail = Task { zone: Some(RAIL_TAP), main: true, ..task.clone() };
            // A rail the power stage planned (it has a current) is searched as often as repair asks:
            // capped here, an benchmark C +5V feed came out thin and failed its fixture (0.26% drop against
            // 0.25%). The cap is for rails with no spec, where the ladder is all there is.
            // (2026-11-10: the memo and the cap of post.rs `rail_route_guarded` were also tried here for
            // rails without a spec. They saved nothing on the board they were meant for, esc-g431, and
            // cost joins on two others whose repair needs the repeats: benchmark C without its
            // spec 300 -> 297, a current-monitor board 521 -> 520. Repair searches unguarded.)
            let made = self.rail_route(&rail);
            if let Some(id) = made {
                return Some(id);
            }
        }
        let mut rules = self.rules.clone();
        rules.node_limit = SOFT_NODES * if self.deep { 6 } else { 1 };
        let (sources, targets) = if task.level == Level::Power && task.a != task.b { self.island_terminals(task) } else { (vec![self.pad_terminal(task.a)], self.targets_of(task)) };
        // At the neck: this is about finding room. A no-via way round is weighed against a via.
        let (w, n) = self.widths_for(task);
        let c = self.rules.clearance_for(task.net, task.net);
        let ladder: &[(u32, f64)] = if task.level == Level::Other { &[(2, 3.0), (2, 8.0), (4, 40.0)] } else { &[(0, 3.0), (2, 3.0), (0, 8.0), (2, 8.0), (4, 40.0)] };
        let profiles: Vec<Profile> = ladder.iter().map(|&(vias, margin)| Profile { width: n, neck: n, clearance: c, target_gap: c, max_vias: vias, margin: margin * NM, layer_bias: None }).collect();
        let (path, _) = self.weigh(task, &sources, &targets, &profiles, &rules)?;
        let pts = self.finish_path(&path, task.net, NO_NET, n);
        let id = self.commit(task, &pts, w, n, NO_NET);
        // A join onto an island's copper is only worth keeping if it really joined.
        if task.level == Level::Power && task.a != task.b && (!self.joined(task) || !self.joints_ok(id)) {
            self.rip(id);
            return None;
        }
        Some(id)
    }

    /// Does this route meet its net's other copper at an obtuse angle? A branch joins as a "Y",
    /// never square or acute (4.2).
    fn joints_ok(&self, id: u32) -> bool {
        let Some(r) = self.routes.get(&id) else { return true };
        for s in &r.segs {
            for (p, far) in [(s.a, s.b), (s.b, s.a)] {
                for o in self.routes.values().filter(|o| o.id != id && o.task.net == r.task.net) {
                    for t in o.segs.iter().filter(|t| t.layer == s.layer) {
                        let other = if t.a == p { Some(t.b) } else if t.b == p { Some(t.a) } else { None };
                        if let Some(q) = other {
                            if turn_of(q, p, far).map(|x| x > 1).unwrap_or(false) {
                                return false;
                            }
                        }
                    }
                }
            }
        }
        true
    }

    fn island_terminals(&self, task: &Task) -> (Vec<Terminal>, Vec<Terminal>) {
        let live = self.islands(task.net);
        let side = |pad: usize| -> (Vec<Terminal>, Vec<Terminal>) {
            let Some(g) = live.iter().find(|g| g.contains(&pad)) else { return (vec![self.pad_terminal(pad)], Vec::new()) };
            let mut points: Vec<Terminal> = g.iter().map(|&p| self.pad_terminal(p)).collect();
            let mut fills: Vec<Terminal> = Vec::new();
            for r in self.routes.values().filter(|r| r.task.net == task.net && (g.contains(&r.task.a) || g.contains(&r.task.b))) {
                for s in &r.segs {
                    points.push(Terminal::Point { p: s.b, layers: vec![s.layer] });
                }
                for v in &r.vias {
                    points.push(Terminal::Point { p: *v, layers: self.world.route_layers.clone() });
                }
                // The fill this island stubs into is part of it: a via down into the ring is how
                // his caps get their +1V1 (4.7).
                if let Some(z) = r.task.zone.filter(|&z| z != RAIL_TAP) {
                    let (_, layer, poly) = &self.world.zones[z];
                    fills.push(Terminal::Zone { layer: *layer, poly: poly.clone() });
                }
            }
            (points, fills)
        };
        let (a, fa) = side(task.a);
        let (mut b, fb) = side(task.b);
        // A fill can only be a target (a source has no one point to start from).
        if fb.is_empty() && !fa.is_empty() {
            b.extend(fa);
            return (b, a);
        }
        b.extend(fb);
        (a, b)
    }

    /// Move a route only as much as the new copper needs: keep what is still legal and search from
    /// the break to the old route. Often what comes out is a shove, since the detour hugs the
    /// copper that displaced it.
    fn splice(&mut self, old: &Route, txn: &mut Txn) -> bool {
        // A tap has no fixed far end to keep: its target is whatever of the rail is nearest now.
        if old.task.a == old.task.b || old.segs.is_empty() {
            return false;
        }
        let task = &old.task;
        let mut pts: Vec<(P, usize)> = Vec::new();
        for s in &old.segs {
            if let Some(&(q, _)) = pts.last() {
                // Tidy-up can drop a piece hidden in the net's own fill: no chain to keep.
                if q != s.a {
                    return false;
                }
            }
            if pts.last() != Some(&(s.a, s.layer)) {
                pts.push((s.a, s.layer));
            }
            pts.push((s.b, s.layer));
        }
        // A route that joined an island elsewhere has no pad to keep.
        if pts[0].0 != snap(self.board.pads[task.a].center) {
            return false;
        }
        let (w, n) = self.widths_for(task);
        let via_q = |net| Query { net, also: None, sources: &[], targets: &[], profile: Profile { width: 0, neck: 0, clearance: self.rules.clearance, target_gap: 0, max_vias: 0, margin: 0.0, layer_bias: None }, rules: &self.rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
        let mut first = None;
        let mut last = 0;
        for (i, x) in pts.windows(2).enumerate() {
            let bad = if x[0].1 == x[1].1 {
                !self.seg_clear(x[0].0, x[1].0, x[0].1, n, task.net, NO_NET)
            } else {
                let (mut b, mut c) = (Vec::new(), 0.0);
                !self.world.via_ok(x[0].0, &via_q(task.net), None, &mut b, &mut c)
            };
            if bad {
                first.get_or_insert(i);
                last = i;
            }
        }
        let Some(first) = first else {
            // The path it was ripped for bent away in tidy-up: nothing to change.
            let id = self.commit(task, &pts, w, n, NO_NET);
            txn.added.push(id);
            return true;
        };
        let mut rules = self.rules.clone();
        rules.node_limit = SOFT_NODES / 2;
        for grow in [0usize, 1, 3] {
            let i = first.saturating_sub(grow);
            let j = (last + 1 + grow).min(pts.len() - 1);
            let term = |k: usize| -> Terminal {
                if k == 0 {
                    self.pad_terminal(task.a)
                } else if k == pts.len() - 1 && task.zone.is_none() {
                    self.pad_terminal(task.b)
                } else {
                    Terminal::Point { p: pts[k].0, layers: vec![pts[k].1] }
                }
            };
            let sources = [term(i)];
            let targets = [term(j)];
            let arrive = if j + 1 < pts.len() && pts[j].1 == pts[j + 1].1 { dir_of(pts[j].0, pts[j + 1].0) } else { None };
            for (vias, margin) in [(0u32, 1.5), (2, 3.0)] {
                let pf = Profile { width: n, neck: n, clearance: self.rules.clearance, target_gap: self.rules.clearance, max_vias: vias, margin: margin * NM, layer_bias: None };
                let q = Query { net: task.net, also: None, sources: &sources, targets: &targets, profile: pf, rules: &rules, soft: None, via_in: None, history: Some(&self.history), free: &[], arrive };
                self.searches += 1;
                let Some(path) = search(&self.world, &q) else { continue };
                self.nodes += path.nodes as u64;
                let mut new: Vec<(P, usize)> = pts[..i].to_vec();
                new.extend(path.pts.iter().copied());
                new.extend(pts[j + 1..].iter().copied());
                simplify(&mut new);
                // The old part and the detour must meet at an obtuse or square angle: a square one
                // gets its chamfer below, an acute one isn't worth keeping.
                let acute = new.windows(3).any(|x| x[0].1 == x[1].1 && x[1].1 == x[2].1 && turn_of(x[0].0, x[1].0, x[2].0).map(|t| t > 2).unwrap_or(false));
                // The kept part's vias are off the board while the detour is found, so the search
                // can't keep clear of them: benchmark C got two holes of one net 0.002 mm apart this way.
                let holes: Vec<P> = new.windows(2).filter(|x| x[0].1 != x[1].1).map(|x| x[0].0).collect();
                let min = (self.rules.drill + self.rules.hole_hole) as f64;
                let crowded = holes.iter().enumerate().any(|(k, a)| holes[k + 1..].iter().any(|b| hypot(sub((*a).into(), (*b).into())) < min));
                if acute || crowded {
                    continue;
                }
                let whole = Path { pts: new, ..Default::default() };
                let tidy = self.finish_path(&whole, task.net, NO_NET, n);
                let id = self.commit(task, &tidy, w, n, NO_NET);
                txn.added.push(id);
                self.splices += 1;
                return true;
            }
        }
        false
    }

    /// Lay this task again, moving what is in the way. True only if it holds, every net it touched
    /// stays whole and nothing was given up; otherwise the board is as it was.
    pub(crate) fn move_for_width(&mut self, task: &Task, open: usize) -> bool {
        let mode = Mode { escalate: true, swap: false };
        let mut txn = Txn { fuel: 14, ..Default::default() };
        let ok = self.displace(task, mode, 3, &mut txn) && self.still_whole(&mut txn, task) && txn.dropped.is_empty() && self.open_pieces(task.net) <= open;
        if !ok {
            self.undo(txn);
        }
        ok
    }

    /// Find this task a place, moving whoever is in the way, who then do the same, `depth` deep.
    fn displace(&mut self, task: &Task, mode: Mode, depth: u32, txn: &mut Txn) -> bool {
        if self.over_time() || txn.fuel <= 0 {
            return false;
        }
        txn.fuel -= 1;
        if task.a == task.b && task.zone.is_none() {
            return self.displace_fanout(task, mode, depth, txn);
        }
        if self.joined(task) || (task.zone == Some(RAIL_TAP) && self.rail_targets(task, 0.0).is_empty()) {
            return true;
        }
        if let Some(id) = self.route_hard(task) {
            txn.added.push(id);
            txn.locked.insert(id);
            return true;
        }
        if depth == 0 {
            return false;
        }
        let Some((path, pf)) = self.soft_path(task, mode, txn) else {
            let why = if RAN_OUT.with(|c| c.get()) { "the search ran out of budget before reaching it, even allowed to move routes" } else { "no path even through the routes it may move: pads, fills or protected copper wall it in" };
            self.why.insert((task.a, task.b), why.to_string());
            return false;
        };
        let victims = self.take_victims(task, &path.blockers, txn);
        if self.trace {
            eprintln!("    move for {} {} -> {}: {:?}", self.board.net_name(task.net), self.board.pad_name(task.a), self.board.pad_name(task.b), victims.iter().map(|r| self.board.net_name(r.task.net).to_string()).collect::<Vec<_>>());
        }
        let (w, _) = self.widths_for(task);
        // The soft lattice has no lines along movable copper, so its path grazes more routes than
        // it must. With them up, the exact lattice finds the hugging path, and any victim it no
        // longer touches goes straight back (splice sees nothing wrong with it).
        let exact = self.route_hard(task);
        let id = match exact {
            Some(id) => id,
            None => {
                let pts = self.finish_path(&path, task.net, NO_NET, pf.neck);
                // A victim's lane came back when it came up, and the soft path couldn't see it:
                // laying that path would box the victim's own pin in.
                let legal = pts.windows(2).all(|x| x[0].1 != x[1].1 || self.seg_clear(x[0].0, x[1].0, x[0].1, pf.neck, task.net, NO_NET));
                if !legal {
                    if let Some(v) = victims.first() {
                        txn.banned.insert((v.task.a, v.task.b));
                    }
                    self.why.insert((task.a, task.b), "the way through crosses another pin's way out".to_string());
                    return false;
                }
                self.commit(task, &pts, w, pf.neck, NO_NET)
            }
        };
        txn.added.push(id);
        txn.locked.insert(id);
        self.relocate(task, victims, mode, depth, txn)
    }

    fn take_victims(&mut self, task: &Task, ids: &[u32], txn: &mut Txn) -> Vec<Route> {
        let me = (task.a, task.b);
        let mut out = Vec::new();
        for b in ids {
            let Some(r) = self.routes.get(b).cloned() else { continue };
            *self.memory.entry((me, (r.task.a, r.task.b))).or_insert(0) += 1;
            if !txn.apart.contains_key(&r.task.net) {
                let n = self.pieces_apart(r.task.net);
                txn.apart.insert(r.task.net, n);
            }
            self.rip(*b);
            self.rips += 1;
            txn.removed.push(r.clone());
            out.push(r);
        }
        out.sort_by_key(|r| r.task.level);
        out
    }

    fn relocate(&mut self, task: &Task, victims: Vec<Route>, mode: Mode, depth: u32, txn: &mut Txn) -> bool {
        for r in victims {
            let before = txn.added.len();
            if self.splice(&r, txn) {
                let added: Vec<u32> = txn.added[before..].to_vec();
                txn.locked.extend(added);
                continue;
            }
            if self.displace(&r.task, mode, depth - 1, txn) {
                continue;
            }
            if mode.swap && r.task.level >= task.level && txn.dropped.is_empty() {
                txn.dropped.push(r.task.clone());
                continue;
            }
            self.why.insert((task.a, task.b), format!("its way is held by {} ({} to {}), which found no other place", self.board.net_name(r.task.net), self.board.pad_name(r.task.a), self.board.pad_name(r.task.b)));
            txn.banned.insert((r.task.a, r.task.b));
            return false;
        }
        true
    }

    /// A pad with no via site in a straight line: a bent stub to the nearest site that has one, and
    /// failing that, a stub to the net's own copper that already reaches the plane, such as a USB
    /// connector's shield pad (reference benchmark B joins J1's GND pins to the shield this way).
    fn fan_out_search(&mut self, task: &Task) -> Option<u32> {
        let p = &self.board.pads[task.a];
        let (center, net) = (p.center, p.net);
        let layer = p.layers.iter().copied().find(|l| self.world.route_layers.contains(l))?;
        let (n, c) = (self.rules.neck, self.rules.clearance);
        let vr = self.rules.via as f64 / 2.0;
        let pad_gap = self.rules.pad_via_gap as f64;
        let shape = p.shape.clone();
        let mut sites: Vec<(f64, P)> = Vec::new();
        let step = 0.1 * NM;
        let reach = (2.5 * NM / step) as i64;
        for j in -reach..=reach {
            for i in -reach..=reach {
                let v = snap((center.0 + (i as f64 * step) as i64, center.1 + (j as f64 * step) as i64));
                let d = hypot(sub(v.into(), center.into()));
                if d > 2.5 * NM || shape.dist_pt(v.into()) < vr + pad_gap {
                    continue;
                }
                let q = Query { net, also: None, sources: &[], targets: &[], profile: Profile { width: 0, neck: 0, clearance: c, target_gap: 0, max_vias: 0, margin: 0.0, layer_bias: None }, rules: &self.rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
                let (mut b, mut sc) = (Vec::new(), 0.0);
                if self.world.via_ok(v, &q, None, &mut b, &mut sc) {
                    sites.push((d, v));
                }
            }
        }
        sites.sort_by(|a, b| a.0.total_cmp(&b.0));
        let mut rules = self.rules.clone();
        rules.node_limit = SOFT_NODES / 4;
        let pf = Profile { width: n, neck: n, clearance: c, target_gap: c, max_vias: 0, margin: 2.0 * NM, layer_bias: None };
        let sources = [self.pad_terminal(task.a)];
        // Sites close together fail together: try a spread of them.
        let mut tried: Vec<P> = Vec::new();
        for (_, v) in sites {
            if tried.len() >= 8 {
                break;
            }
            if tried.iter().any(|t| hypot(sub((*t).into(), v.into())) < 0.3 * NM) {
                continue;
            }
            tried.push(v);
            let targets = [Terminal::Point { p: v, layers: vec![layer] }];
            let q = Query { net, also: None, sources: &sources, targets: &targets, profile: pf, rules: &rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
            self.searches += 1;
            let Some(path) = search(&self.world, &q) else { continue };
            self.nodes += path.nodes as u64;
            let pts = self.finish_path(&path, net, NO_NET, n);
            let id = self.fresh_id();
            let segs: Vec<Seg> = pts.windows(2).filter(|w| w[0].1 == w[1].1 && w[0].0 != w[1].0).map(|w| Seg { a: w[0].0, b: w[1].0, layer: w[0].1, width: n }).collect();
            self.place(id, task.clone(), segs, vec![v]);
            return Some(id);
        }
        // Own copper already on the plane: through-hole pads and vias of this net.
        let mut targets: Vec<Terminal> = Vec::new();
        let planes = self.board.plane_nets();
        let inner = planes.get(&net).cloned().unwrap_or_default();
        let own_fp = self.board.pads[task.a].fp;
        for (i, q) in self.board.pads.iter().enumerate() {
            // Through-hole pads reach the plane; a pin beside its own part's exposed pad joins that
            // (benchmark C's U6.5 and U8.5: no via site in the row, and the pad is right there).
            let exposed = q.fp == own_fp && q.size.0 >= 1.0 * NM && q.size.1 >= 1.0 * NM;
            if i != task.a && q.net == net && (exposed || q.layers.iter().any(|l| inner.contains(l))) && q.layers.contains(&layer) && hypot(sub(q.center.into(), center.into())) < 6.0 * NM {
                targets.push(self.pad_terminal(i));
            }
        }
        for r in self.routes.values().filter(|r| r.task.net == net) {
            for v in r.vias.iter().filter(|v| hypot(sub((**v).into(), center.into())) < 6.0 * NM) {
                targets.push(Terminal::Point { p: *v, layers: vec![layer] });
            }
        }
        if targets.is_empty() {
            return None;
        }
        let pf = Profile { margin: 3.0 * NM, ..pf };
        let q = Query { net, also: None, sources: &sources, targets: &targets, profile: pf, rules: &rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
        self.searches += 1;
        let path = search(&self.world, &q)?;
        let pts = self.finish_path(&path, net, NO_NET, n);
        let id = self.fresh_id();
        let segs: Vec<Seg> = pts.windows(2).filter(|w| w[0].1 == w[1].1 && w[0].0 != w[1].0).map(|w| Seg { a: w[0].0, b: w[1].0, layer: w[0].1, width: n }).collect();
        self.notes.push(format!("{}: joined to the net's own plane copper, no via site of its own (4.4)", self.board.pad_name(task.a)));
        self.place(id, task.clone(), segs, Vec::new());
        Some(id)
    }

    fn displace_fanout(&mut self, task: &Task, mode: Mode, depth: u32, txn: &mut Txn) -> bool {
        let before = self.next_id;
        if self.fan_out_pad(task.a, task) {
            for id in before..self.next_id {
                txn.added.push(id);
                txn.locked.insert(id);
            }
            return true;
        }
        if let Some(id) = self.fan_out_search(task) {
            txn.added.push(id);
            txn.locked.insert(id);
            return true;
        }
        if depth == 0 {
            return false;
        }
        let prices = self.prices_for(task, mode, txn);
        let Some((v, layer, blockers)) = self.fan_out_site(task.a, &prices) else {
            self.why.insert((task.a, task.b), "no via site within 2 mm, even moving the routes it may move".to_string());
            return false;
        };
        let victims = self.take_victims(task, &blockers, txn);
        let id = self.next_id;
        self.next_id += 1;
        self.place(id, task.clone(), vec![Seg { a: snap(self.board.pads[task.a].center), b: v, layer, width: self.rules.neck }], vec![v]);
        txn.added.push(id);
        txn.locked.insert(id);
        self.relocate(task, victims, mode, depth, txn)
    }

    /// No net a repair touched may end up in more pieces than it was in, bar a swap's one hole.
    fn still_whole(&mut self, txn: &mut Txn, task: &Task) -> bool {
        if self.loose {
            return true;
        }
        let mut ok = true;
        for (net, was) in txn.apart.clone() {
            let spare = txn.dropped.iter().filter(|t| t.net == net).count();
            if self.pieces_apart(net) > was + spare {
                ok = false;
                let hit: Vec<(usize, usize)> = txn.removed.iter().filter(|r| r.task.net == net).map(|r| (r.task.a, r.task.b)).collect();
                txn.banned.extend(hit);
                self.why.insert((task.a, task.b), format!("moving {} for it would take that net apart", self.board.net_name(net)));
            }
        }
        ok
    }

    /// One task's repair, whole or not at all. A victim that couldn't move is banned for the next
    /// try, which so has to find another way; the same failure twice ends it.
    pub(super) fn repair_one(&mut self, task: &Task, mode: Mode) -> Repair {
        let mut banned = HashSet::new();
        for _ in 0..REPAIR_TRIES {
            let mut txn = Txn { banned: banned.clone(), fuel: 14, ..Default::default() };
            let ok = self.displace(task, mode, 3, &mut txn) && self.still_whole(&mut txn, task);
            if ok {
                return match txn.dropped.pop() {
                    Some(t) => Repair::Swapped(t),
                    None => Repair::Fixed,
                };
            }
            let learned = txn.banned.len() > banned.len();
            banned = std::mem::take(&mut txn.banned);
            self.undo(txn);
            if !learned || self.over_time() {
                break;
            }
        }
        Repair::Failed
    }

    /// Part 3 step 8, the move a plain re-search can't make: this connection has a good way but someone
    /// sits on it. Take it, then let them find another place. Kept only if the board's cost falls and
    /// no net comes apart. The caller has already lifted the route; on failure it goes back.
    /// `own_pieces`: how apart the route's own net was before it was lifted.
    pub(crate) fn swap_for(&mut self, old: &Route, old_cost: f64, own_pieces: usize) -> bool {
        let task = old.task.clone();
        let mode = Mode { escalate: false, swap: false };
        let mut txn = Txn { fuel: 8, ..Default::default() };
        let Some((path, _)) = self.soft_path(&task, mode, &txn) else { return false };
        if path.blockers.is_empty() || path.blockers.len() > 3 {
            return false;
        }
        if path.blockers.iter().any(|b| self.routes.get(b).map(|r| self.in_bus_row(&r.task)).unwrap_or(false)) {
            return false;
        }
        let nets: Vec<NetId> = {
            let mut v: Vec<NetId> = path.blockers.iter().filter_map(|b| self.routes.get(b).map(|r| r.task.net)).collect();
            v.push(task.net);
            v.sort_unstable();
            v.dedup();
            v
        };
        let pieces: usize = nets.iter().map(|&n| if n == task.net { own_pieces } else { self.open_pieces(n) }).sum();
        let before: f64 = path.blockers.iter().filter_map(|b| self.routes.get(b)).map(|r| self.route_cost(r)).sum::<f64>() + old_cost;
        let victims = self.take_victims(&task, &path.blockers, &mut txn);
        let done = match self.route_hard(&task) {
            Some(id) => {
                txn.added.push(id);
                txn.locked.insert(id);
                self.relocate(&task, victims, mode, 1, &mut txn)
            }
            None => false,
        };
        let after: f64 = txn.added.iter().filter_map(|id| self.routes.get(id)).map(|r| self.route_cost(r)).sum();
        let whole = nets.iter().map(|&n| self.open_pieces(n)).sum::<usize>() <= pieces;
        if self.trace {
            eprintln!("    swap {} {}: blockers {:?} placed {done} whole {whole} cost {:.1} -> {:.1}", self.board.net_name(task.net), self.board.pad_name(task.a), nets.iter().map(|&n| self.board.net_name(n)).collect::<Vec<_>>(), before / NM, after / NM);
        }
        if done && whole && after + 0.05 * NM < before {
            return true;
        }
        self.undo(txn);
        false
    }

    /// A 90-degree corner that tidy-up couldn't chamfer for lack of room breaks 4.2 outright: go
    /// around again, with square corners priced out of the search. Stays as it was if that fails.
    pub(super) fn fix_square_corners(&mut self) {
        let square = |r: &Route| -> bool {
            r.segs.windows(2).any(|s| s[0].layer == s[1].layer && s[0].b == s[1].a && turn_of(s[0].a, s[0].b, s[1].b) == Some(2))
        };
        // A fan-out stub has no second pad to route to: searching would lay nothing and lose its via.
        let ids: Vec<u32> = self.routes.values().filter(|r| square(r) && !matches!(r.task.level, Level::Pair | Level::Kelvin) && !(r.task.a == r.task.b && r.task.zone.is_none())).map(|r| r.id).collect();
        let mut rules = self.rules.clone();
        rules.cost_bend90 = 30.0 * NM;
        rules.node_limit = SOFT_NODES * if self.deep { 6 } else { 1 };
        for id in ids {
            let Some(old) = self.routes.get(&id).cloned() else { continue };
            self.rip(id);
            let sources = [self.pad_terminal(old.task.a)];
            let targets = self.targets_of(&old.task);
            let (w, n) = self.widths_for(&old.task);
            let mut placed = false;
            for (vias, margin) in [(old.vias.len() as u32, 3.0), (old.vias.len() as u32 + 2, 8.0)] {
                let pf = Profile { width: n, neck: n, clearance: self.rules.clearance, target_gap: self.rules.clearance, max_vias: vias, margin: margin * NM, layer_bias: None };
                let q = Query { net: old.task.net, also: None, sources: &sources, targets: &targets, profile: pf, rules: &rules, soft: None, via_in: None, history: None, free: &[], arrive: None };
                let Some(path) = search(&self.world, &q) else { continue };
                let pts = self.finish_path(&path, old.task.net, NO_NET, n);
                let new = self.commit(&old.task, &pts, w, n, NO_NET);
                if self.routes.get(&new).map(square).unwrap_or(true) {
                    self.rip(new);
                    continue;
                }
                placed = true;
                break;
            }
            if !placed {
                self.place(old.id, old.task, old.segs, old.vias);
            }
        }
    }

    /// The teardrop KiCad will draw where this route leaves a pad, as a polygon: from the pad's edge
    /// to a point half the pad's size along the track (at most 1 mm), and as wide as the pad where it
    /// meets it. Read from the project's teardrop settings as the reference boards carry them, and checked
    /// against the one KiCad reported on piprobe_v13 (its side point, 173.26 by 67.75, is this
    /// polygon's). Follows the track round one bend, as KiCad's two-track teardrops do.
    fn teardrop_at(&self, pad: usize, chain: &[(P, i64)]) -> Option<(usize, Vec<V>)> {
        let p = &self.board.pads[pad];
        let b = p.shape.bbox();
        let size = (b.2 - b.0).min(b.3 - b.1);
        let (first, w) = chain.first().copied()?;
        if w as f64 >= 0.9 * size || chain.len() < 2 {
            return None;
        }
        let _ = first;
        // Walk out of the pad.
        let mut exit = None;
        for k in 0..chain.len() - 1 {
            let (u, v): (V, V) = (chain[k].0.into(), chain[k + 1].0.into());
            if p.shape.dist_pt(v) > 0.0 {
                let l = hypot(sub(v, u)).max(1.0);
                let mut t = 0.0;
                while t < l {
                    let q = V(u.0 + (v.0 - u.0) * t / l, u.1 + (v.1 - u.1) * t / l);
                    if p.shape.dist_pt(q) > 0.0 {
                        break;
                    }
                    t += 0.01 * NM;
                }
                exit = Some((k, V(u.0 + (v.0 - u.0) * t / l, u.1 + (v.1 - u.1) * t / l), V((v.0 - u.0) / l, (v.1 - u.1) / l)));
                break;
            }
        }
        let (k, out, dir) = exit?;
        let mut left = (0.5 * size).min(1.0 * NM);
        let mut at = out;
        let mut along = dir;
        for j in k..chain.len() - 1 {
            let v: V = chain[j + 1].0.into();
            let l = hypot(sub(v, at));
            if l >= left {
                at = V(at.0 + along.0 * left, at.1 + along.1 * left);
                break;
            }
            left -= l;
            at = v;
            if j + 2 < chain.len() {
                let n: V = chain[j + 2].0.into();
                let m = hypot(sub(n, v)).max(1.0);
                along = V((n.0 - v.0) / m, (n.1 - v.1) / m);
            }
        }
        let half = w as f64 / 2.0;
        let h = (size / 2.0).min(1.0 * NM);
        let (na, nd) = (V(-along.1, along.0), V(-dir.1, dir.0));
        let clamp = |q: V| V(q.0.clamp(b.0, b.2), q.1.clamp(b.1, b.3));
        Some((0, vec![
            V(at.0 + na.0 * half, at.1 + na.1 * half),
            V(at.0 - na.0 * half, at.1 - na.1 * half),
            clamp(V(out.0 - nd.0 * h, out.1 - nd.1 * h)),
            clamp(V(out.0 + nd.0 * h, out.1 + nd.1 * h)),
        ]))
    }

    /// Who a route's future teardrops would bridge: (pad on the route, foreign pad, layer).
    fn teardrop_hits(&self, r: &Route) -> Vec<(usize, usize, usize)> {
        let mut out = Vec::new();
        let by_layer: Vec<usize> = { let mut v: Vec<usize> = r.segs.iter().map(|s| s.layer).collect(); v.dedup(); v };
        for layer in by_layer {
            let run: Vec<&Seg> = r.segs.iter().filter(|s| s.layer == layer).collect();
            if run.is_empty() {
                continue;
            }
            let mut fwd: Vec<(P, i64)> = vec![(run[0].a, run[0].width)];
            fwd.extend(run.iter().map(|s| (s.b, s.width)));
            let back: Vec<(P, i64)> = fwd.iter().rev().copied().collect();
            for chain in [fwd, back] {
                let start: V = chain[0].0.into();
                let pad = self.board.pads.iter().position(|p| p.net == r.task.net && p.layers.contains(&layer) && p.shape.dist_pt(start) <= 1.0);
                let Some(pad) = pad else { continue };
                if let Some((_, poly)) = self.teardrop_at(pad, &chain) {
                    if let Some((who, _)) = self.world.mask.poly_near(r.task.net, layer, &poly, 0.03 * NM) {
                        out.push((pad, who, layer));
                    }
                }
            }
        }
        out
    }

    /// KiCad draws teardrops at refill and its DRC then judges them, so a track that clears a pad's
    /// mask opening can still have a teardrop that bridges it. Find those now and route them again
    /// wide of that opening; a route that can't go wide stays and KiCad will say so.
    pub(super) fn fix_teardrops(&mut self) {
        // Benchmark B's project ignores mask bridges, so this would chase what nobody judges.
        if self.rules.severity("solder_mask_bridge") != "error" {
            return;
        }
        // Not a fan-out stub: it has no second pad to route to, and "routing" one would lay nothing.
        let ids: Vec<u32> = self.routes.values().filter(|r| !r.segs.is_empty() && !matches!(r.task.level, Level::Pair | Level::Kelvin) && !(r.task.a == r.task.b && r.task.zone.is_none())).map(|r| r.id).collect();
        for id in ids {
            let Some(old) = self.routes.get(&id).cloned() else { continue };
            let hits = self.teardrop_hits(&old);
            if hits.is_empty() {
                continue;
            }
            self.unplace(id);
            let mut done = false;
            for wide in [0.4, 0.25, 0.15] {
                let mut held = Vec::new();
                for &(_, who, layer) in &hits {
                    let b = self.board.pads[who].shape.bbox();
                    let m = wide * NM;
                    let s = Shape::Poly { pts: vec![V(b.0 - m, b.1 - m), V(b.2 + m, b.1 - m), V(b.2 + m, b.3 + m), V(b.0 - m, b.3 + m)], r: 0.0 };
                    held.push((layer, self.world.layers[layer].add(s, NO_NET, Owner::Keepout)));
                }
                let before = self.next_id;
                let placed = if old.task.zone == Some(RAIL_TAP) { self.try_rail(&old.task) } else { self.try_route_inner(&old.task) };
                for (l, h) in held {
                    self.world.layers[l].remove(h);
                }
                if placed && self.next_id > before {
                    let new = self.next_id - 1;
                    let ok = self.routes.get(&new).map(|r| self.teardrop_hits(r).is_empty()).unwrap_or(false) && self.pieces_apart(old.task.net) <= 1;
                    if ok {
                        done = true;
                        break;
                    }
                    self.unplace(new);
                }
            }
            if done {
                self.log.push(format!("teardrop: {} moved clear of a pad's mask opening", self.board.net_name(old.task.net)));
            } else {
                let task = old.task.clone();
                self.place(old.id, task, old.segs.clone(), old.vias.clone());
                self.notes.push(format!("{}: KiCad's teardrop at {} may bridge {}'s mask opening; no other way in was found", self.board.net_name(old.task.net), self.board.pad_name(hits[0].0), self.board.pad_name(hits[0].1)));
            }
        }
    }

    /// Plane-net pads that the plan left to an outer-layer fill: that fill is the old board's, and
    /// KiCad's refill around the new tracks can cut a pad off it (benchmark C U6.5, U8.5; benchmark B
    /// U3.10). Each gets its own via where one stands (4.2: individual vias for each passive). No
    /// site is no failure here: the fill may well hold, and KiCad's DRC is the judge of that.
    pub(super) fn stitch_fill_pads(&mut self) {
        let planes = self.board.plane_nets();
        let mut count = 0;
        let mut pads: Vec<usize> = self.net_info.iter().filter(|(n, (l, _))| *l == Level::PlaneFanout && planes.contains_key(n)).flat_map(|(_, (_, p))| p.iter().copied()).collect();
        pads.sort_unstable();
        for pad in pads {
            let net = self.board.pads[pad].net;
            let inner = &planes[&net];
            // An exposed pad has its thermal vias already: piprobe_v13's U1 EP got a stub across the
            // +1V1 ring from here before this test.
            let size = self.board.pads[pad].size;
            if (size.0 >= 1.0 * NM && size.1 >= 1.0 * NM) || self.board.pads[pad].layers.iter().any(|l| inner.contains(l)) || self.routes.values().any(|r| r.task.a == pad || r.task.b == pad) {
                continue;
            }
            let task = Task { net, level: Level::PlaneFanout, a: pad, b: pad, main: false, zone: None };
            if self.fan_out_pad(pad, &task) || self.fan_out_search(&task).is_some() {
                count += 1;
            }
        }
        if count > 0 {
            self.log.push(format!("{count} plane-net pads that relied on an old fill got their own via"));
        }
    }

    /// A stuck connection usually isn't stuck by the board, only by the few routes laid around its
    /// pin before it had a turn. Lift everything near its ends, lay it first, and lay them back. Much
    /// cheaper than a restart, and it can't lose ground: the board stays as it was unless every
    /// route lifted finds a place again.
    fn local_restart(&mut self, task: &Task, radius: f64) -> bool {
        let ends: Vec<V> = vec![self.board.pads[task.a].center.into(), self.board.pads[task.b].center.into()];
        let near = |r: &Route| -> bool {
            ends.iter().any(|&e| r.segs.iter().any(|s| pt_seg(e, s.a.into(), s.b.into()) <= radius) || r.vias.iter().any(|v| hypot(sub(e, (*v).into())) <= radius))
        };
        let mut ids: Vec<u32> = self.routes.values().filter(|r| near(r) && !matches!(r.task.level, Level::Pair | Level::Kelvin) && !(r.segs.is_empty() && r.task.a == r.task.b)).map(|r| r.id).collect();
        ids.sort_unstable();
        if ids.is_empty() {
            return false;
        }
        let mut txn = Txn { fuel: ids.len() as i32 * 2 + 4, ..Default::default() };
        let victims = self.take_victims(task, &ids, &mut txn);
        let mode = Mode { escalate: true, swap: false };
        let mut ok = self.displace(task, mode, 0, &mut txn);
        if ok {
            for r in victims {
                let before = txn.added.len();
                if self.splice(&r, &mut txn) {
                    let added: Vec<u32> = txn.added[before..].to_vec();
                    txn.locked.extend(added);
                } else if !self.displace(&r.task, mode, 1, &mut txn) {
                    ok = false;
                    break;
                }
            }
        }
        if ok && self.still_whole(&mut txn, task) {
            return true;
        }
        self.undo(txn);
        false
    }

    /// Rounds over everything still open until a round closes nothing. Then one round of swap
    /// moves, which can walk a hole to a place where it closes; the best state seen is what counts.
    pub(super) fn repair(&mut self, failed: Vec<Task>, best: &mut Option<Best>) -> Vec<Task> {
        let mut open = self.open_work(failed);
        self.keep_if_best(&open, best);
        let mut swaps_left = SWAP_ROUNDS;
        let mut swapped: HashSet<(usize, usize)> = HashSet::new();
        while !open.is_empty() && !self.over_time() {
            let swap = swaps_left > 0 && self.stalled;
            let mut still = Vec::new();
            let before = open.len();
            let mut work: std::collections::VecDeque<Task> = open.into();
            while let Some(task) = work.pop_front() {
                if self.over_time() {
                    still.push(task);
                    continue;
                }
                let t0 = Instant::now();
                let mode = Mode { escalate: true, swap: swap && !swapped.contains(&(task.a, task.b)) };
                // One stuck spot may not eat the run: benchmark B spent 73 s on two tasks.
                let deadline = self.rules.deadline;
                let turn = Instant::now() + std::time::Duration::from_secs_f64(self.rules.time_limit / 10.0);
                self.rules.deadline = deadline.map(|d| d.min(turn));
                let mut r = self.repair_one(&task, mode);
                if matches!(r, Repair::Failed) && task.a != task.b {
                    for radius in [0.8, 1.5, 3.0] {
                        if self.local_restart(&task, radius * NM) {
                            r = Repair::Fixed;
                            self.log.push(format!("{} {}: stuck pin freed by lifting what's within {radius} mm", self.board.net_name(task.net), self.board.pad_name(task.a)));
                            break;
                        }
                    }
                }
                self.rules.deadline = deadline;
                if self.trace {
                    eprintln!("  {:<7.2}s repair {} {} {} -> {} {}", t0.elapsed().as_secs_f64(), match r { Repair::Fixed => "fixed", Repair::Swapped(_) => "swap ", Repair::Failed => "FAIL " }, task.level.name(), self.board.pad_name(task.a), self.board.pad_name(task.b), self.board.net_name(task.net));
                }
                match r {
                    Repair::Fixed => {}
                    Repair::Swapped(t) => {
                        swapped.insert((task.a, task.b));
                        still.push(t);
                    }
                    Repair::Failed => still.push(task),
                }
            }
            open = self.open_work(still);
            self.keep_if_best(&open, best);
            // Only fewer holes counts as progress. The same holes again means the next round
            // would repeat this one exactly, unless a swap changes the board.
            if open.len() < before {
                self.stalled = false;
                continue;
            }
            if self.stalled {
                swaps_left = swaps_left.saturating_sub(1);
                if swaps_left == 0 {
                    break;
                }
            }
            self.stalled = true;
        }
        self.stalled = false;
        self.fix_square_corners();
        self.keep_if_best(&open, best);
        open
    }
}

/// One net's way out of a connector: its stubs on the pads' layer, vias, and any short jog onto a
/// run already on the far layer.
#[derive(Clone)]
struct ExitPlan {
    net: NetId,
    pads: Vec<usize>,
    vias: Vec<P>,
    top: Vec<(P, P)>,
    bottom: Vec<(P, P)>,
    /// Pads of the part that the plan passes inside their class clearance: each needs a local rule.
    close: Vec<(usize, f64)>,
    cost: f64,
}

/// Search reach and how many ways per net go into the combined search.
/// The most of a net that may lie in a big pour once its way out is settled: a stub off a pad.
pub(super) const STUB_RUN: f64 = 1.2 * NM;
const EXIT_STEP: i64 = 50_000;
const EXIT_KEEP: usize = 24;
/// A jog from a via onto its own run, longest and shortest: he stands 0.2 mm off (4.4 "where there is room").
const JOG_MAX: f64 = 0.9 * NM;
const JOG_MIN: f64 = 0.15 * NM;

impl<'a> Router<'a> {
    /// Copper of `net` on `layer` from a to b: clear of everything by class, except this part's own
    /// pads, which may come as close as the standing minimum and are noted for a local rule.
    fn exit_clear(&self, a: P, b: P, half: f64, layer: usize, net: NetId, fp: usize, close: &mut Vec<(usize, f64)>) -> bool {
        let (va, vb): (V, V) = (a.into(), b.into());
        let floor = self.rules.floor.max(self.rules.min.clearance) as f64;
        let reach = half + self.rules.max_clearance() as f64 + self.world.max_local + 1.0;
        let bb = (va.0.min(vb.0) - reach, va.1.min(vb.1) - reach, va.0.max(vb.0) + reach, va.1.max(vb.1) + reach);
        let mut ok = true;
        self.world.layers[layer].visit(bb, |o| {
            if o.net == net || self.world.crosses_pour(o, net) {
                return true;
            }
            let (mine, local) = match o.owner {
                Owner::Pad(i) => (self.board.pads[i as usize].fp == fp, self.world.pad_clearance[i as usize]),
                _ => (false, 0.0),
            };
            let class = (self.rules.clearance_for(net, o.net) as f64).max(local);
            let need = half + if mine { floor.max(local) } else { class };
            let d = o.dist_within(va, vb, (half + class) * 1.01);
            if d < need - DRC_TOL {
                ok = false;
                return false;
            }
            if let (true, Owner::Pad(i)) = (mine && d < half + class - DRC_TOL, o.owner) {
                close.push((i as usize, d - half));
            }
            true
        });
        ok && self.world.mask.seg_hit(net, layer, va, vb, half).is_none() && self.world.holes_clear(&self.rules, va, vb, half, net)
    }

    fn exit_via_ok(&self, s: P, net: NetId, fp: usize, close: &mut Vec<(usize, f64)>) -> bool {
        let (dia, drill) = self.rules.via_for(net);
        let vr = dia as f64 / 2.0;
        let own = vr + self.rules.pad_via_gap as f64;
        let v: V = s.into();
        if self.board.pads.iter().any(|p| p.net == net && !p.layers.is_empty() && hypot(sub(p.center.into(), v)) < 3.0 * NM && p.shape.dist_pt(v) < own) {
            return false;
        }
        if self.world.mask.via_hit(net, v, vr).is_some() || !self.world.holes_clear(&self.rules, v, v, vr, net) {
            return false;
        }
        let hole = drill as f64 / 2.0 + self.rules.hole_to_hole() as f64;
        let mut clear = true;
        self.world.holes.visit((v.0 - hole, v.1 - hole, v.0 + hole, v.1 + hole), |o| {
            if o.shape.dist_pt(v) < hole {
                clear = false;
                return false;
            }
            true
        });
        if !clear {
            return false;
        }
        (0..self.board.layers.len()).filter(|&l| self.board.layers[l].signal).all(|l| self.exit_clear(s, s, vr, l, net, fp, close))
    }

    /// Pad centre to a via: straight if it can be, else one 45-degree bend either way round.
    fn exit_stub(&self, c: P, s: P, layer: usize, net: NetId, fp: usize, close: &mut Vec<(usize, f64)>) -> Option<Vec<(P, P)>> {
        self.exit_stub_at(c, s, layer, net, fp, self.rules.neck, close)
    }

    fn exit_stub_at(&self, c: P, s: P, layer: usize, net: NetId, fp: usize, width: i64, close: &mut Vec<(usize, f64)>) -> Option<Vec<(P, P)>> {
        let half = width as f64 / 2.0;
        let (dx, dy) = (s.0 - c.0, s.1 - c.1);
        let (ax, ay) = (dx.abs(), dy.abs());
        let m = ax.min(ay);
        let (sx, sy) = (dx.signum(), dy.signum());
        let diag = (sx * m, sy * m);
        let straight = if ax > ay { (sx * (ax - ay), 0) } else { (0, sy * (ay - ax)) };
        let mut ways: Vec<Vec<(P, P)>> = Vec::new();
        if m == 0 || ax == ay {
            ways.push(vec![(c, s)]);
        } else {
            // Straight off the pad first (reference exits), then the 45.
            let e1 = snap((c.0 + straight.0, c.1 + straight.1));
            let e2 = snap((c.0 + diag.0, c.1 + diag.1));
            ways.push(vec![(c, e1), (e1, s)]);
            ways.push(vec![(c, e2), (e2, s)]);
        }
        for w in ways {
            let mut got = Vec::new();
            if w.iter().all(|(a, b)| self.exit_clear(*a, *b, half, layer, net, fp, &mut got)) {
                close.extend(got);
                return Some(w);
            }
        }
        None
    }

    /// From a via, the shortest straight jog onto the net's run on `layer`, standing off it (4.4).
    fn exit_jog(&self, v: P, layer: usize, net: NetId, fp: usize, close: &mut Vec<(usize, f64)>) -> Option<(P, P, f64)> {
        let segs: Vec<Seg> = self.routes.values().filter(|r| r.task.net == net).flat_map(|r| r.segs.iter().cloned()).filter(|s| s.layer == layer).collect();
        let at: V = v.into();
        let mut best: Option<(P, P, f64)> = None;
        for s in &segs {
            if pt_seg(at, s.a.into(), s.b.into()) < JOG_MIN {
                return None;
            }
            let (sa, sb): (V, V) = (s.a.into(), s.b.into());
            for (dx, dy) in DIRS {
                let (ux, uy) = (dx as f64, dy as f64);
                let (ex, ey) = (sb.0 - sa.0, sb.1 - sa.1);
                let denom = ux * ey - uy * ex;
                if denom.abs() < 1e-6 {
                    continue;
                }
                // A branch joins as a Y, never square (4.2).
                if (ux * ex + uy * ey).abs() < 1e-6 * hypot(V(ex, ey)) {
                    continue;
                }
                let (wx, wy) = (sa.0 - at.0, sa.1 - at.1);
                let t = (wx * ey - wy * ex) / denom;
                let u = (wx * uy - wy * ux) / denom;
                let len = t * hypot(V(ux, uy));
                if t <= 0.0 || !(0.0..=1.0).contains(&u) || len > JOG_MAX {
                    continue;
                }
                let hit = (at.0 + ux * t, at.1 + uy * t);
                let q = (hit.0.round() as i64, hit.1.round() as i64);
                if best.map(|b| len < b.2).unwrap_or(true) {
                    let half = s.width as f64 / 2.0;
                    let mut got = Vec::new();
                    if self.exit_clear(v, q, half, layer, net, fp, &mut got) {
                        close.extend(got);
                        best = Some((v, q, len));
                    }
                }
            }
        }
        best
    }

    /// Every way worth trying for one net's pads on this part, cheapest first.
    fn exit_ways(&self, net: NetId, pads: &[usize], fp: usize, far: usize, on_run: bool) -> Vec<ExitPlan> {
        let top = self.board.pads[pads[0]].layers[0];
        let reach = (POUR_RUN as i64) / EXIT_STEP;
        let centers: Vec<P> = pads.iter().map(|&p| snap(self.board.pads[p].center)).collect();
        let mid = (centers.iter().map(|c| c.0).sum::<i64>() / centers.len() as i64, centers.iter().map(|c| c.1).sum::<i64>() / centers.len() as i64);
        let mut out: Vec<ExitPlan> = Vec::new();
        for ix in -reach..=reach {
            for iy in -reach..=reach {
                let s = snap((mid.0 + ix * EXIT_STEP, mid.1 + iy * EXIT_STEP));
                let v: V = s.into();
                if centers.iter().any(|c| {
                    let (ax, ay) = ((c.0 - s.0).abs() as f64, (c.1 - s.1).abs() as f64);
                    ax.max(ay) + (SQRT2 - 1.0) * ax.min(ay) > POUR_RUN
                }) {
                    continue;
                }
                let mut close = Vec::new();
                if !self.exit_via_ok(s, net, fp, &mut close) {
                    continue;
                }
                let mut topway = Vec::new();
                if !centers.iter().all(|c| match self.exit_stub(*c, s, top, net, fp, &mut close) {
                    Some(w) => {
                        topway.extend(w);
                        true
                    }
                    None => false,
                }) {
                    continue;
                }
                let mut bottom = Vec::new();
                if on_run {
                    match self.exit_jog(s, far, net, fp, &mut close) {
                        Some((a, b, _)) => bottom.push((a, b)),
                        None => continue,
                    }
                }
                let len = |w: &[(P, P)]| w.iter().map(|(a, b)| hypot(sub((*a).into(), (*b).into()))).sum::<f64>();
                let mut rule_pads: Vec<usize> = close.iter().map(|c| c.0).collect();
                rule_pads.sort_unstable();
                rule_pads.dedup();
                // Each exception weighs what 0.5 mm of stub does: fewest first, but not at any price.
                let cost = len(&topway) + 0.5 * len(&bottom) + 0.5 * NM * rule_pads.len() as f64;
                let _ = v;
                out.push(ExitPlan { net, pads: pads.to_vec(), vias: vec![s], top: topway, bottom, close, cost });
            }
        }
        out.sort_by(|a, b| a.cost.total_cmp(&b.cost));
        // Spread out, or the search only sees one spot many times over.
        let mut kept: Vec<ExitPlan> = Vec::new();
        for w in out {
            if kept.iter().all(|k| hypot(sub(k.vias[0].into(), w.vias[0].into())) >= 0.1 * NM) {
                kept.push(w);
                if kept.len() >= EXIT_KEEP {
                    break;
                }
            }
        }
        kept
    }

    fn exits_fit(&self, a: &ExitPlan, b: &ExitPlan) -> bool {
        let clr = self.rules.clearance_for(a.net, b.net) as f64 - DRC_TOL;
        let half = self.rules.neck as f64 / 2.0;
        let vr = |p: &ExitPlan| self.rules.via_for(p.net).0 as f64 / 2.0;
        let (ra, rb) = (vr(a), vr(b));
        let hole = (self.rules.via_for(a.net).1 + self.rules.via_for(b.net).1) as f64 / 2.0 + self.rules.hole_to_hole() as f64;
        for &x in &a.vias {
            for &y in &b.vias {
                let d = hypot(sub(x.into(), y.into()));
                if d < ra + rb + clr || d < hole {
                    return false;
                }
            }
            for (p, q) in b.top.iter().chain(&b.bottom) {
                if pt_seg(x.into(), (*p).into(), (*q).into()) < ra + half + clr {
                    return false;
                }
            }
        }
        for &y in &b.vias {
            for (p, q) in a.top.iter().chain(&a.bottom) {
                if pt_seg(y.into(), (*p).into(), (*q).into()) < rb + half + clr {
                    return false;
                }
            }
        }
        for (list_a, list_b) in [(&a.top, &b.top), (&a.bottom, &b.bottom)] {
            for (p, q) in list_a {
                for (r, s) in list_b {
                    if seg_seg((*p).into(), (*q).into(), (*r).into(), (*s).into()) < 2.0 * half + clr {
                        return false;
                    }
                }
            }
        }
        true
    }

    /// Lay every exit of this connector as one plan: the cheapest combination in which all of its nets
    /// fit together. The nets of the run through it join it by a jog; the others keep a via their own
    /// routes start from. False leaves the board untouched.
    pub(super) fn plan_exits(&mut self, fp: usize, run: [NetId; 2], far: usize) -> bool {
        let mut by_net: Vec<(NetId, Vec<usize>)> = Vec::new();
        for &i in &self.board.footprints[fp].pads {
            if !self.pour_exit_pads.contains(&i) || looks_ground(self.board.net_name(self.board.pads[i].net)) {
                continue;
            }
            let net = self.board.pads[i].net;
            match by_net.iter_mut().find(|x| x.0 == net) {
                Some(x) => x.1.push(i),
                None => by_net.push((net, vec![i])),
            }
        }
        if by_net.is_empty() {
            return false;
        }
        // A net's pads share a via if that works; otherwise each has its own, as his A7 and B7 do.
        let mut options: Vec<Vec<Vec<ExitPlan>>> = Vec::new();
        for (net, pads) in &by_net {
            let on_run = run.contains(net);
            let mut ways: Vec<Vec<ExitPlan>> = self.exit_ways(*net, pads, fp, far, on_run).into_iter().map(|w| vec![w]).collect();
            if pads.len() == 2 {
                let (a, b) = (self.exit_ways(*net, &pads[..1], fp, far, on_run), self.exit_ways(*net, &pads[1..], fp, far, on_run));
                for x in a.iter().take(10) {
                    for y in b.iter().take(10) {
                        if hypot(sub(x.vias[0].into(), y.vias[0].into())) >= self.rules.via_for(*net).0 as f64 + 0.15 * NM {
                            ways.push(vec![x.clone(), y.clone()]);
                        }
                    }
                }
            }
            if ways.is_empty() {
                self.log.push(format!("exits: {} has no legal way off {}", self.board.net_name(*net), self.board.footprints[fp].reference));
                return false;
            }
            let cost = |w: &Vec<ExitPlan>| w.iter().map(|p| p.cost).sum::<f64>();
            ways.sort_by(|a, b| cost(a).total_cmp(&cost(b)));
            ways.truncate(EXIT_KEEP * 2);
            options.push(ways);
        }
        // The tightest net first, so a dead end shows early.
        options.sort_by_key(|w| w.len());
        let mut best: Option<(f64, Vec<usize>)> = None;
        let mut pick = vec![0usize; options.len()];
        self.exit_search(&options, 0, 0.0, &mut pick, &mut best);
        let Some((_, chosen)) = best else {
            self.log.push(format!("exits: no combination fits at {}", self.board.footprints[fp].reference));
            return false;
        };
        let name = self.board.footprints[fp].reference.clone();
        let far_layer = far;
        for (k, &c) in chosen.iter().enumerate() {
            for plan in options[k][c].clone() {
                let top = self.board.pads[plan.pads[0]].layers[0];
                let neck = self.rules.neck;
                let run_w = self.routes.values().filter(|r| r.task.net == plan.net).flat_map(|r| r.segs.iter()).find(|s| s.layer == far_layer).map(|s| s.width).unwrap_or(neck);
                let mut segs: Vec<Seg> = plan.top.iter().filter(|(a, b)| a != b).map(|(a, b)| Seg { a: *a, b: *b, layer: top, width: neck }).collect();
                segs.extend(plan.bottom.iter().filter(|(a, b)| a != b).map(|(a, b)| Seg { a: *a, b: *b, layer: far_layer, width: run_w }));
                let (a, b) = (plan.pads[0], *plan.pads.last().unwrap());
                let task = Task { net: plan.net, level: Level::Pair, a, b, main: false, zone: None };
                let id = self.fresh_id();
                self.place(id, task, segs, plan.vias.clone());
                self.exit_tasks.insert((a, b));
                if !run.contains(&plan.net) {
                    for &p in &plan.pads {
                        self.exit_via.insert(p, plan.vias[0]);
                    }
                }
                // Exceptions, each at what the plan really leaves.
                let net_name = self.board.net_name(plan.net).to_string();
                let mut seen: HashMap<usize, f64> = HashMap::new();
                for (pad, room) in &plan.close {
                    let e = seen.entry(*pad).or_insert(*room);
                    *e = e.min(*room);
                }
                for (pad, room) in seen {
                    let number = self.board.pads[pad].number.clone();
                    let min = ((room / 1000.0).floor() as i64 * 1000).max(self.rules.floor.max(self.rules.min.clearance));
                    if !self.rules.pad_rules.iter().any(|r| r.net == net_name && r.reference == name && r.pad == number && r.min <= min) {
                        self.rules.pad_rules.retain(|r| !(r.net == net_name && r.reference == name && r.pad == number));
                        self.rules.pad_rules.push(PadRule { net: net_name.clone(), reference: name.clone(), pad: number, min });
                    }
                }
            }
        }
        self.rules.bind(self.board);
        self.log.push(format!("exits: planned {} nets off {} as one group", chosen.len(), name));
        true
    }

    fn exit_search(&self, options: &[Vec<Vec<ExitPlan>>], k: usize, cost: f64, pick: &mut Vec<usize>, best: &mut Option<(f64, Vec<usize>)>) {
        if best.as_ref().map(|b| cost >= b.0).unwrap_or(false) {
            return;
        }
        if k == options.len() {
            *best = Some((cost, pick.clone()));
            return;
        }
        for (c, way) in options[k].iter().enumerate() {
            let fits = (0..k).all(|j| options[j][pick[j]].iter().all(|a| way.iter().all(|b| self.exits_fit(a, b))));
            if !fits {
                continue;
            }
            pick[k] = c;
            self.exit_search(options, k + 1, cost + way.iter().map(|p| p.cost).sum::<f64>(), pick, best);
        }
    }

    /// A stub from a pad to the nearest through-hole pad of its net on the same part, if one is close.
    pub(super) fn tie_to_sibling(&mut self, pad: usize, task: &Task) -> bool {
        const TIE_MAX: f64 = 2.5 * NM;
        let p = self.board.pads[pad].clone();
        let Some(&layer) = p.layers.first() else { return false };
        let c = snap(p.center);
        let net = p.net;
        let fp = p.fp;
        let mut near: Vec<(f64, P)> = self.board.footprints[fp].pads.iter().copied().filter(|&q| q != pad).map(|q| &self.board.pads[q]).filter(|q| q.net == net && q.layers.len() > 1 && q.layers.contains(&layer)).map(|q| (hypot(sub(q.center.into(), p.center.into())), snap(q.center))).filter(|x| x.0 <= TIE_MAX + 2.0 * NM).collect();
        near.sort_by(|a, b| a.0.total_cmp(&b.0));
        for (_, to) in near {
            for width in [mm(0.4), self.rules.track, self.rules.neck] {
                let mut close = Vec::new();
                if let Some(way) = self.exit_stub_at(c, to, layer, net, fp, width, &mut close) {
                    if !close.is_empty() {
                        continue;
                    }
                    let segs: Vec<Seg> = way.iter().filter(|(a, b)| a != b).map(|(a, b)| Seg { a: *a, b: *b, layer, width }).collect();
                    let id = self.fresh_id();
                    self.place(id, Task { level: Level::Pair, zone: None, main: false, ..task.clone() }, segs, Vec::new());
                    return true;
                }
            }
        }
        false
    }

    /// After the best board is put back: which pads start from a planned via.
    pub(super) fn rebuild_exit_vias(&mut self) {
        self.exit_via.clear();
        let run_nets: Vec<(usize, usize, P)> = self.routes.values().filter(|r| self.exit_tasks.contains(&(r.task.a, r.task.b)) && !r.vias.is_empty() && !r.segs.iter().any(|s| s.layer != self.board.pads[r.task.a].layers[0])).map(|r| (r.task.a, r.task.b, r.vias[0])).collect();
        for (a, b, v) in run_nets {
            self.exit_via.insert(a, v);
            self.exit_via.insert(b, v);
        }
    }
}