soldermaskdocs

Parts and packages

resolve.resolve(design) turns the model's intent into a netlist of real, orderable parts with footprints, and records everything it could not do.

Where parts come from

Three sources answer in order, and each one falls through to the next rather than to nothing.

The parametric index answers a value part exactly. 100nF in 0603 is not a phrase to match against a description — it is two sets of ids in the catalogue's own attribute table (attributes-lut.json.gz, 242,213 parsed (name, value) pairs in SI base units) and the parts that carry both. It matters because the question has a right answer: asked for 100nF, a 220nF is not a near miss to rank below, it is wrong, and no similarity score tells close in the catalogue's prose from close in farads. Measured over 105 jellybean queries — E-series values across 0402/0603/0805, scored against the attribute table, both arms offline:

Armcorrect@1wrongfound nothingmedian
Text search (FTS5 bm25)61 %833537.5 ms
The parametric index99 %013.6 ms

The one it found nothing for is 100 µF in 0402, which is not manufactured — the smallest real package is 1206 — so that is the right answer too. The index lives in the cache and is built by tools/parametric_index.py; when it is absent the caller keeps the ladder below, because a missing index must degrade to the previous behaviour, never to no parts. See soldermask/params.py.

The other 23 % of a board is named by a manufacturer part number, which is a lexical problem and not a semantic one. The failure there is shape — a model writing AP2112K-3.3 from memory where the distributor lists AP2112K-3.3TRG1 produced nothing at all. Over 150 real chips, five ways a model might write each number:

Query form@1@10found nothing
Exact93 %95 %4 %
Packaging suffix dropped29 %29 %65 %
Trailing letters dropped0 %3 %94 %
Hyphens removed3 %3 %97 %

Normalising the punctuation away and matching on a prefix fixes all four, because each is the same part number wearing different marks.

The catalogue on disk comes from JLCPCB itself when the machine has API credentials, and from the jlcparts mirror otherwise. JLCPCB's open API (jlcapi.py; an application, three environment variables, a signed request) has no search — a lookup by code, a listing and a cursor-paged dump — so its place is the source the local catalogue is written from: soldermask setup --parts takes the dump (the whole LCSC listing, 7,251,747 records on 16 Sep 2026, 54 minutes at ~2,200 a second, of which 723,240 have stock), then a detail record for every code with stock (1,000 codes a call, under a second each — the parameters are only there), and jlcapi.ingest() writes the files below in the mirror's own format, so every reader is unchanged and the attribute table and the rows are one process's work from one snapshot. What that buys was measured the same day, every row of the mirror on disk against the vendor's dump: the manufacturer part number, which the mirror carries in the row, agreed on 582,737 of 582,888; the package, which it carries as an index into a daily-reordered attribute table, agreed on 168,603, disagreed on 259,001 and was blank on 155,262 — an RP2040 read as a 144-ball BGA, an AD9288 as a DIP-28, seven whole shards of chip resistors wrong on every row — because the table on disk was another day's and the manifest, rebuilt from disk on 15 Sep, could no longer say so. Parts with no stock are left out of the catalogue (min_stock=1; the raw dump under .cache/jlcapi/<day>/ keeps them all, so the choice costs no network to revisit), and a manifest the API wrote is never replaced by the mirror's on the daily TTL.

Without credentials, the mirror (yaqwsx/jlcparts, MIT — 582,650 components at the 2026-09-04 build). jlcsearch is downstream of it, so this is the same data one party closer to the source; it was verified rather than assumed, C328062 coming back from both with stock 2527 and the identical six price breaks, character for character. A part costs two small cached fetches — a lookup bucket and a shard — not a database. It also carries what jlcsearch does not: joints, the pad count, which otherwise has to be inferred from a package name, and the full parametric attribute set the index above is built from.

The mirror's answer is the answer, including an empty one. A part is its LCSC code and the mirror holds every code jlcsearch does, so a search that finds nothing on disk is not sent over the network to the same data again. Until 15 Sep 2026 it was: every empty local result went on to jlcsearch, two to four requests a part, and one query the server never answered (Green LED 0805 SMD with the package filter) cost every board with a green LED thirty seconds a build, uncached because only successes were. Measured that day over the 59 evaluation designs, the mirror alone and the mirror with jlcsearch behind it chose the same part for every component.

jlcsearch remains behind the mirror for a machine the mirror cannot answer on — no manifest and no way to fetch one, a shard that will not read, an index that will not open — so that degrades rather than stops (no key; responses cached for 24 hours under .cache/parts, a failure remembered for an hour beside them so it is not asked again mid-build, a 10 s wait, and a stale cache used when the network is down — stale beats nothing). It is a free, unauthenticated, no-SLA server run by a competitor, and the first full orderability run showed what that costs: 40 of its 76 “unsourceable” parts were its rate limiter, not JLCPCB's catalogue. Its typed endpoints for resistors and capacitors disagree with its primary endpoint on stock and price, so a part it did find is re-read from the primary endpoint and the reconciliation is noted. Set SOLDERMASK_PARTS=jlcsearch to pin that path when comparing the two.

Live stock at order time still belongs to inventory.py, by exact LCSC code: a snapshot is a snapshot however fresh, and the question there is can I buy N of these now, which no mirror answers.

The query ladder

Where a component is not a value part with an index entry, the search is widened step by step until something orderable turns up:

  1. the search phrase as written;
  2. its first word, if it looks like a part number (has both a letter and a digit);
  3. the value;
  4. the first word with a trailing alphabetic ordering suffix trimmed — LMV358IDR → LMV358, 1N5819WS → 1N5819. A trailing digit is never stripped: trimming AMS1117-3.3 to AMS1117 would let the search return the 5 V part, which places, routes, passes ERC and destroys whatever the board powers;
  5. generic functional terms for the kind (pin header, USB-C, crystal, …).

Candidates must fit the declared package (for headers: 2.54 mm pitch and the right pin count — JLCPCB names every 2.54 mm header 插件,P=2.54mm with no count, and matching on package alone once ordered a 2-pin header for a 4-pad footprint), and must not be in a category that contradicts the kind. Among survivors the order of preference is: in stock, category confirmed, pin count confirmed, basic before extended, more stock, then price. Stock is a preference, not a gate: a real part that shows 0 today still resolves, with a note saying so, because stock changes hourly and losing a finished board to it answers the wrong question.

An unresolved part is dropped from the netlist and recorded as a gap: R3: no JLCPCB part exists for '…' in 0603. A network outage is reported as an outage — could not reach JLCPCB to check stock … this is an outage here, not a statement about the part — never as a missing part.

Basic and extended

JLCPCB charges a setup fee for every distinct extended (non-basic) part; the pipeline counts it at $3 each in the BOM total and the stock report, and the model is asked to prefer basic parts.

Supported packages

The hand-written, IPC-style footprints the model may choose from:

0402 0603 0805 1206 · SOT-23 SOT-23-3 SOT-223 SOD-123 · SOIC-8 SOP-8 SOIC-16 TSSOP-20 · HDR-1x2 HDR-1x3 HDR-1x4 HDR-1x6 HDR-1x8

plus any footprint from the imported KiCad corpus (soldermask/assets/footprints.json, built by import_footprints.py) whose name follows standard nomenclature — chip sizes, SOIC/SOP/TSSOP/MSOP/QFN/DFN/QFP, SOT/TO-/DPAK, LED, USB, PinHeader, Crystal, SW_, Fuse, and so on — with at most 48 pins, because beyond this, a 2-layer autoroute is fantasy. Hand-written geometry wins name clashes. JLCPCB spellings are canonicalised (0603 (1608 Metric) → 0603, SOT23-3 → SOT-23-3, SOIC-8_150mil → SOIC-8).

One more name is offered that is not a footprint at all: FROM_PART, meaning no package is being named. A module, a radio, a radar has no IPC name and exactly one correct footprint — the vendor's, for the part being ordered — which is the path below. Before it existed the model had no way to say so, and asked for a board with an ESP32 module it emitted two pin headers and no ESP32.

Every package carries pads (position, size, shape, through-hole and drill), a body outline and a courtyard margin (0.25 mm), and a 3D profile — height, lead style (chip, gullwing, tab, noleads, dome, header, block, shell, button) and standoff — used to draw a part when no vendor model exists.

Footprints from the vendor

When a package is not one JLCPCB names — 59 % of the names offered to the model can never match a JLCPCB package string, and FROM_PART says so on purpose — the chosen part's own footprint is fetched from EasyEDA (cached hard under .cache/easyeda, since it is an undocumented endpoint and a build should not need the network twice for the same part). The board then carries a note: … so the footprint is C12345's own (8 pads, 4.9×3.9 mm) as published by the vendor — not IPC-verified, check it before ordering. Where a vendor footprint can be compared against the nominal package, pads more than 0.35 mm off at the best orientation are reported, never silently reconciled: only a person with the datasheet can tell a variant from a bad footprint.

Alternates

alternates.py proposes replacements for a part that failed to resolve, drawn from parts that share the exact same KiCad symbol pinout — ground truth, not similarity. 84 % of such classes are one family differing only by a number (AMS1117-1.5 versus -3.3: pin-identical, electrically nothing alike), so an alternate is only marked safe to swap when the rated value can be read from both part numbers and matches; otherwise it is offered as pinout-only for a person to confirm.

On this page