Your graph knows what touches what. It still does not know what anything is
TLDR: We could detect 1272 wires and 193 shapes in an Arduino schematic and still not produce a netlist, because a netlist is made of names. The text layer sits beside the geometry with no link between them, and reconstructing that link is a separate problem with its own rules.
Two halves that do not reference each other
A vector drawing gives you shapes with coordinates. A text layer gives you strings with coordinates. Nothing in the file says that the string R1 describes the box eight points to its left.
That association was in the designer's head, and then in the CAD tool's internal model, and it did not survive export. What survived is proximity.
So the whole semantic layer rests on a spatial join, and the interesting work is in the rules that make the join correct rather than merely plausible.
Rule one: the vocabulary is small, and that is the point
The naive designator test is letters followed by digits. On our sheet it matched 167 strings.
The document contains 36 designators.
The other 131 were pin names: A0, ADC12, PA7, PB0. A microcontroller sheet has far more pin labels than parts, so the naive rule is wrong about four times out of five, and every wrong match then competes for a symbol and can win.
Designator prefixes are a standardised set. R, C, L, D, Q, U, IC, JP, T, Y, X, RN and a couple of dozen more, defined by IEC 81346 and used more or less consistently by everyone. Matching against the alphabet rather than the shape took the match count from 167 to 36, which is exactly the number of parts on the sheet.
The general form of this: when a domain has a controlled vocabulary, use the vocabulary. A pattern that describes the shape of the token will always also describe the things that merely look like it, and in real documents the lookalikes outnumber the real ones.
Rule two: assign globally, not per-item
The obvious loop is: for each symbol, find the nearest designator.
That double-books. Two parts side by side both find the single label between them, and the second one takes a name belonging to its neighbour. Worse, it fails silently, because both answers look reasonable in isolation.
Build every (label, symbol) pair within range, sort by distance, and consume both sides as they match. Each label gets exactly one owner and each symbol gets exactly one name, and the closest pairs win. It is a greedy assignment rather than an optimal one, but on this kind of data the distance gaps are large and greedy is indistinguishable from optimal.
The same structure applies to values and part numbers, with one ordering constraint: designators are assigned first, and values only attach to symbols that already have one. Otherwise an unidentified blob near a capacitor absorbs 100nF and the capacitor gets nothing.
Rule three: same name means same net, even with no wire between them
This is the one that changes the output most, and it has no geometric equivalent.
A designer writes GND on eight stubs precisely so they do not have to draw a wire between them. +5V crosses a sheet without a single continuous conductor anywhere on it. The label is the connection.
Geometry alone therefore reports the most important nets on the board as the most fragmented. Our first run found +5V eight separate times, +3V3 four times, each a small isolated run touching two or three parts.
Merging nets by name turned 165 named runs into 70 nets, and turned +5V into a single rail with 62 wire segments touching 14 parts. That is the difference between a list of drawn line-runs and a netlist.
It is worth keeping the count of merged runs on each net. A reader deserves to see that a rail was assembled from thirteen separate drawn pieces rather than traced as one conductor, because that is a claim they may want to check.
Rule four: separate the layers without hardcoding the palette
Before any of this you have to know which strokes are wires and which are symbol outlines. In our file they are geometrically identical: a resistor's body is four straight stroked segments and so is a piece of wiring.
Colour separates them, but which colour means what is a per-tool and often per-user setting, so a lookup table is a trap.
What is invariant is the shape of the two populations. A net connects things that are far apart, so its segments chain into runs that span the sheet. A symbol outline stays inside its own symbol, so its segments chain into runs a few millimetres across.
Measured on our sheet: the net colour's connected runs have a median diameter of 57.6pt, the symbol colour's 8.0pt, the frame colour's 3.7pt. That is not a marginal separation and it does not depend on anyone's colour scheme. Group strokes by colour, measure median connected-run span per group, and the layer roles fall out of the drawing itself.
Report the evidence, not the score
Our first coverage number was symbol clusters minus named clusters: 115 unnamed. It read as a failure and it was meaningless, because most clusters are junction dots and pin stubs, not parts.
The number a reader actually needs is how much of the naming evidence was consumed. 36 designators in the document, 35 resolved, one unresolved and named. That is checkable. It also tells you the ceiling: you cannot name parts a drawing never labelled, and pretending otherwise by inferring designators from geometry would be inventing data.
The same applies to the layer split. Publishing the median run span per colour alongside the netlist lets a reader see why the tool believed green was the net layer, and disagree if it was wrong.
What remains ambiguous
One part came out as M7 = 47uF. M7 is the package marking printed on a diode, sitting next to that diode's real designator D1, and 47uF belongs to a capacitor whose own designator was not in the text layer. The spatial join has no way to know that; both strings are near both parts.
That is one error in thirty-five, and it is the honest shape of this problem. A drawing is a picture made for a person who already knows the conventions, and some of what it means was never written down.