Order guide parser
Deterministic parser for Audi of America Order Guide PDFs, in
packages/ai_access_audi. Audi calls them Order Guides, so the module does too;
the Stellantis equivalent in ai_dealer_connect is a Code Guide and keeps that
name.
The parser reads what a guide says. The records that follow from one are a consumer's own — this package defines none.
from ai_access_audi.order_guides.parser import parse, parse_document
guide = parse("Audi Order Guide 2027 USA (Invoice & Retail) 08.10.2026 - FINAL.pdf")
guide.page_count # 94
guide.page_text(59) # the Q7 quattro model page
document = parse_document(...)
document.identity.lineage_key # 'audi|USA|2027'
Status
Wired: identity, the contents table, page kinds and model page ranges (parser);
the column grid (tables); a model page's header band, trim bands and options
(grids); the accessories table, the color matrix and the interior components
(colors); and compatibility qualifiers (notes).
Read across both US guides: 55 models over 60 variants, 117 trim bands, 3,646 features, 515 option codes, 444 accessories, 441 colors, 2,059 color cells, 252 seat-trim columns, 604 compatibility rules, 42 market restrictions and 7 editorial notes.
Not yet read: the Audi exclusive pages and the Updates table. exclusive_options
and updates raise NotImplementedError, and AudiGuideDocument carries no
field for either — an empty field on the envelope means the guide prints nothing,
never that nothing looked.
What identifies a qualifier
Color opens the question and the text settles it. A guide sets every
compatibility clause in red and nothing black — Audi sound system (10 speakers, 180 watts) is parenthesized and black and states no rule;
21” black optic package (n/a with CN5) is red and states one. So the color
disposes of the two clauses that read like rules and are not:
limited time sub., accept. of Terms req'd names no code, and
requires 1D6, helps with maneuvering a trailer is prose inside a black feature
name. Neither is red, so neither is parsed as a rule.
But red is not only for qualifiers. There are two reds — #c00000 for 741 spans
and #ff0000 for 134 — and the guides use each for four different things, one
2026 page setting one clause in each. So a red span's meaning comes from its
text, with the qualifier as the default and the guides' three fixed notices out:
| What a red span states | How it is recognized | Spans | Where it is read |
|---|---|---|---|
| a compatibility qualifier | anything the three below are not | 755 | qualifier and rules on the record it follows |
| a market restriction | the sentence Not Available for Vehicles sold in Alaska and Hawaii | 42 | PortInstalledAccessory.market_restriction |
| a model, trim or accessory closed to ordering | NO LONGER AVAILABLE FOR ORDERING, NOT AVAILABLE FOR ORDERING, NOT AVAILABLE in any case | 72 | ContentsEntry.unavailable_note, and unavailable_stamped_pages in each fixture's manifest |
| an editorial note to the reader | opens with * | 6 | OptionGroup.notes |
notes.is_qualifier makes that call, so nothing else has to. A caller wanting
the red text whatever it states asks notes.join_red instead — which is what the
contents table's name cell does, because there the red is the field.
Two reds that state neither: the discount sentence beside a CRT row is Calibri
rather than AudiType, and a - cell and a $-600 price are read from the column
they fall in, never as text.
Guide anatomy
Measured on the 2026 (107 pages) and 2027 (94 pages) US guides, both stamped 08/10/2026.
| Section | Contents |
|---|---|
| Cover | Audi of America, Model Year 2027, Order Guide, Invoice & Retail, the edition date |
| Notices | Signature Care, a tariff and pricing note — set in Aptos, not AudiType |
| Table of Contents | one row per vehicle: name, printed page, and a model code split across a body-style, engine and drivetrain column, with Date Updated: stamping the edition |
| Model page | the header band, the STANDARD FEATURES table banded by trim, and the OPTIONS table |
| Port Installed Accessories | a code grid, then a description / code / retail / invoice table |
| Exterior & Interior | an exterior-color × seat-trim availability matrix, INTERIOR COMPONENT OPTIONS, and inlay and wheel images with text captions |
| Audi exclusive | a section cover, an ordering FAQ, then per-model special paint and tailored interior tables |
| Addendum | the low-aspect-ratio tire note |
| Updates | model / update / date rows |
A model occupies a header page plus the accessories and color pages after it. A model
whose build window splits mid-year prints two header pages sharing one accessories and
color set, titled ...: Before CW 36/26 and ...: From SOP CW 36/26 — four such pairs
in the 2027 guide and one in the 2026. The two windows share a model code and base price
and re-code and re-price the trims: the A6 Sportback e-tron is WPS $3,300 / WPT $7,900
before CW 36/26 and XPP $3,900 / XPR $8,500 from SOP. So each window is a
ModelVariant of the one model rather than a model of its own.
An OPTIONS table that runs past the foot of its page continues on the page beneath,
which carries no heading of its own — pages 60 and 74 of the 2027 guide, page 29 of the
2026. That page belongs to the variant above it, and its rows are read into the same
ModelVariant.
The model page
Page 59 of the 2027 guide, 2027 Q7 quattro, at measured x positions:
ENGINE DRIVETRAIN / TRANSMISSION MODEL CODE RETAIL INVOICE
2.9L TFSI, 6 CYL. 429 HP quattro® Tiptronic® G8BAYA $70,900 $66,647
Destination Charge (subject to change, add to all orders): $1,395 $1,395
Metallic or pearl effect paint: $595 $559
TRIM STANDARD FEATURES OPTION CODE RETAIL INVOICE
20" 5-arm aero design wheels Heated front seats
PREMIUM Audi drive select Power tailgate
PREMIUM INCLUDES PREMIUM FEATURES IN ADDITION TO:
PLUS Dashcam Power soft-closing doors WPS $4,400 $4,136
PRESTIGE INCLUDES PREMIUM PLUS FEATURES IN ADDITION TO:
Adaptive air suspension Head-up display WPT $14,000 $13,160
PREMIUM PREMIUM PLUS PRESTIGE OPTIONS OPTION CODE RETAIL INVOICE
21” black optic package (n/a with CN5)
- O O 21" 5-V-spoke wheels Black exterior trim XBO $1,750 $1,645
Three things follow from this:
- Feature and option lists run in two columns (x≈73 and x≈280 on the features table, x≈111 and x≈273 on the options table), so reading order is down the left column of a band then down the right — not line by line across the page.
- Trim bands cascade: a band after the first opens with
INCLUDES <trim> FEATURES IN ADDITION TO:and lists only what that trim adds. - Where a page has more than one trim, the options table carries a mark column per trim.
XandOmean offered,Sstandard, a dash not offered. Nothing else appears in either guide; lowercasexoccurs 90 times against 1,908 uppercase, and one row of each guide sets its dash as an em dash rather than the hyphen — 2026 p85 and 2027 p72 both print Chili Red metallic againstMAthat way. A dash reads as not offered whichever of the two it is, because the alternative is reading it as no data.
Prices
Whole dollars, read as int. Of the 2,433 $ tokens across both guides exactly
one carries a full stop — $1.295, beside the $1,395 every other model prints
as its destination charge on both sides — so the stop is a comma set wrongly and
not a cent. A float would invite a precision the source never states, and a
Decimal a fraction it never has.
Four tokens break the common $40,700 shape, and all four are real prices:
| Token | Where | What it is |
|---|---|---|
$-600 / $-565 | 2026 p28, code CRT | a package discount, signed rather than bracketed |
$1000 | 2027 p8, code U43 | printed without its thousands comma |
$1.295 | 2027 p31, S8 destination | printed with a full stop for the comma |
A discount is attached to a trim rather than offered as an option: the 2026
guide closes its XPP and XPR bands with a red row pricing CRT $-600 $-565
and the sentence ALL VEHICLES WITH XPP—PREMIUM PLUS PACKAGE WILL ALSO RECEIVE CRT—PACKAGE DISCOUNT. Dropping it overstates every such vehicle by $600, so it
is read as the trim's discount.
That sentence is Calibri in #ff0000 — an editorial overlay, like a page stamp —
while its code and price cells are AudiType in the same red in the table beneath
it. Neither is a qualifier: the sentence is separated by its face, and the cells
by the columns they fall in, which are read as a code and a price rather than as
text.
Why the tables are hand-rolled
The guides are produced by Microsoft Publisher and embed the AudiType family, so their text extracts cleanly at span level. Their tables do not: a guide draws no vertical ruling lines at all, only a shaded header band and hairline row separators. Every table-detecting library therefore finds the header row and nothing else.
Measured on page 59 of the 2027 guide, which holds the three-trim OPTIONS grid above:
| Approach | Result |
|---|---|
pymupdf find_tables() | 3 tables, each 1 row — the header bands only |
pdfplumber extract_tables(lines) | 3 tables, each 1 row — the same headers |
pymupdf find_tables(strategy="text") | 1 table, 96 rows, cells split mid-word |
pdfplumber extract_tables(text) | 1 table, 98 rows, cells split mid-word |
pymupdf4llm to_markdown() | right shape, dropped glyphs in the text |
pymupdf4llm is the closest of the five and still unusable: it renders
Auto-dimming interior mirror with compass and Integrated Toll Module (ITM) as
Adii ii i ih d Id Tll Mdl ITM.
What the guides do give up is a column grid. The header band and each row separator are emitted as runs of filled rectangles cut at every column edge — page 59's OPTIONS table separates at
(18.0, 47.7) (47.7, 78.8) (78.8, 108.3) (108.3, 270.6) (270.6, 463.0)
(463.0, 472.5) (472.5, 506.2) (506.2, 515.2) (515.2, 548.5) (548.5, 557.5)
(557.5, 590.2)
which is the three trim columns, the two content columns, the option code column and the
two price columns, with a 9pt gutter piece between each pair. So the segmentation of one
bar carries what a vertical rule would carry elsewhere. layout exposes those rectangles
as Bars and tables recovers the grid from them.
That is the same shape as the Stellantis parser in ai_dealer_connect, which reads its
tables off ruling lines and font weights, and it keeps the dependency the same: PyMuPDF
and nothing else.
Gotchas
Titles. Body text tops out at 6pt; every title is 11pt or more in
AudiType-ExtendedBold. The first non-blank line is not a title signal — pages 99–107
of the 2026 guide lead with a bare page number. A page with no AudiType heading above
body size has no title of its own and continues the page before it: the tariff notice on
page 3 of each guide, and the OPTIONS tables that overflowed a model page (60 and 74 in
the 2027 guide, 29 in the 2026).
A title also wraps, and a page can set a second line at the same size that is not part
of it — page 18 of the 2027 guide stamps SOLD ORDER ONLY in the title face. The gap
tells them apart: 19–20pt between the lines of one title, 30pt or more before anything
else set at that size.
Unavailable stamps. A guide marks a page whose model is closed to ordering with a
stamp (NO LONGER AVAILABLE FOR ORDERING, NOT AVAILABLE FOR ORDERING) — five pages of
the 2027 guide and 35 of the 2026 guide. Three ways to get it wrong: it is a Word overlay
set in Calibri or Arial on most pages but AudiType in red at title size on 2026 pp. 14,
17, 42, 54, 57, 60, 63 and 98–106; the overlay is split across spans on three pages of the
2026 guide, so a span-level read truncates it to NO LONGER AV; and the Updates table
states the same words as prose about an option code, so a match against the page's full
text over-reports. Matching against the page's non-AudiType spans plus its AudiType spans
set in red at 10pt or more handles all three — the contents table's own red
Not Available for Ordering cells and the Updates prose are both at body size.
Qualifiers. Compatibility is printed inline in parentheses — 188 distinct clauses in
the 2027 guide, 208 in the 2026. The forms are req. X, n/a with X, comma and slash
lists, req. X and Y/Z, and req. FW interior where a code is followed by the noun it
names. Two clauses in the corpus state no rule at all and must not become one:
limited time sub., accept. of Terms req'd and
requires 1D6, helps with maneuvering a trailer. Each fixture ships its full corpus as
qualifier_corpus.json so a new form shows up as a diff.
Two clauses can sit on one line. A page runs its content in two halves, so one visual line carries a cell from each and each half states a qualifier of its own. Spacing tells them apart: across the two guides the widest gap inside one clause is 7.4pt and the narrowest between two is 15.3pt, so red spans more than 10pt apart are separate clauses.
A requires clause is a boolean expression. req. XDA and XBL/XBR/XBE or XSP names
five codes and is satisfied by XSP alone or by XDA with one of the other three, so a flat
code list cannot state it. OptionRule.alternatives holds the sets which each satisfy the
rule. and binds tighter than or and /; a comma stands in for whichever of the two
the clause spells out, so req. XC2, PCR, PNY needs all three while
req. MP, RJ or RH interior needs one. A comma set before the and inverts the binding —
requires YVU or YVQ, and YSR or YTU is one of each pair. OR is both the connective and
a real seat-trim code, so it is read as a code wherever splitting on it would leave a term
with none.
A group can be ordered under more than one code, one per interior — 30 groups across the two guides. Three printed forms, all naming the interior beside the code:
PWE (req. FW) the code and its own clause, one row per code
PWF (req. II)
PWE/PWF one row, the name line's clause slash-split to match
(FW) PWW the interior first, stating the same requirement
(IP) PWJ
Each code is separately orderable and separately conditioned, so each is an OptionGroup
of its own; the name, contents, price and trim marks are the group's and repeat across its
codes.
A group name can open in lower case. ultra package on pages 17, 18, 21 and 22 of the
2027 guide is a group, not a wrapped continuation of the one above it. What does continue a
bold name is an unclosed bracket on the line above — the 2026 guide's
Gear shift lever … (Forced on all vehicles produced from CW37/25 followed by onwards).
An editorial note is bold and opens with *. The 2026 guide states seven and the
2027 guide none: *CURRENT PRODUCTION DELAY (p17),
*ORDERING WITHOUT CB2 WILL RESULT IN PRODUCTION DELAY (pp54, 60),
*ORDERING W/O X56 WILL RESULT IN PRODUCTION DELAY (pp57, 63),
*ORDERING WILL RESULT IN PRODUCTION DELAYS (p42) and
* Note: XM2 wheel replaces C2C wheel from KW23 and beyond (p23). Each addresses ordering
the group it sits under, so each is an OptionGroup.notes entry rather than part of the
name or the contents. Six of the seven sit beside the name on its own row rather than on a
row of their own, and the guide sets one in #ff0000, five in #c00000 and one in black —
so the note is recognized by weight and by the *, never by its color or its position.
A seat-trim column can name an order code as well as its trim code. (GX) XBL,
(FW) PWE — 32 columns across the two guides. The code is what tells two columns of one
trim apart: page 48 of the 2027 guide offers FW plain and again under PWE, page 13
offers GX plain and again under XBL. It states the same thing req. XBL would, so
SeatTrimColumn.option_code records it and rules carries the requirement. Where the
guide instead separates two columns of one trim by a clause (page 16's (FW) against
(FW) (req. PWW)) or by the seat style above them (page 10 of the 2026 guide offers EI
under both Leatherette / Dinamica® and Sport Seats), that is what distinguishes them.
Wrapped lines. A feature or option-content line that wraps is set at the same
left edge and the same leading as the line it continues, so nothing in the geometry
separates the two. How it begins does: of the 5,313 content lines across the two
guides, 4,698 start with a capital and 554 with a digit, and every one of the
remaining 61 is a wrap — (40/20/40), front cross-traffic assist),
season run flat tires, & center console sides.
A qualifier's brackets are black. Only the clause inside them is red, and the
two can land in different spans, so removing the red leaves a lone ( or )
behind. On page 70 of the 2027 guide the qualifier wraps and its closing bracket
lands alone on the next line, bold — a line that also carries the previous group's
content. A group therefore opens on its bold text alone, never on the whole line.
Seat-trim columns come in three shapes. Page 7 sets the code on a row of its
own (AO); page 66 sets it after the description in one cell (Black (MP)); page
13 sets it in brackets ahead of an unrelated option code ((GX) XBL). A column's
code can also wrap onto a line by itself when its description runs long, as (JN)
does on page 82.
The column grid comes from cells, not spans. A guide breaks a header cell
across spans wherever it likes: page 16 of the 2027 guide sets (NK) as a red (
at x 293.8 and a black NK) at x 295.8, and page 53 of the 2026 guide sets
EI (req. XBP or XSP) as a black EI, a red ( and the red rest. Spans that
touch are one cell — across both guides the widest gap inside a cell is 0.09pt
and the narrowest between two columns is 7.6pt — and the cell is what states a
code. It is also where the guide centers the column: the marks under page 53's
fourth column sit at x 537.0, which is the whole cell's center at 536.3 and not
the EI span's at 515.6.
A description is centered over its own column and is wider than it, so the
center places it rather than its order among the row's spans. Three things break
a positional pairing: a description that wraps onto a row of its own (2026 p53's
stitching under Parchment Beige / Steel Gray), the black brackets around a red
qualifier taking spans of their own (2027 p51 prints seven labels over five
columns), and a dropped column shifting everything after it. The description rows
are the last row above the codes together with any row within 9pt above it, which
is the font's leading; anything further above opens the seat-style headings, which
some pages set at the same size as a description and others larger.
A column's own qualifier is printed on either row. Page 53 of the 2026 guide
sets it beside the code and page 91 beside the description, so both rows are read
for it. Where the guide sets the column's order code in red instead — 2026 p31's
(FW) PWW — that code is not a qualifier.
A seat-trim code is not unique across a page. Page 40 offers GX in two
columns under different seat styles, so a color's marks are positional — one per
column, aligned with seat_trims — rather than keyed by code. Two columns of one
trim are always separated by something: the order code beside it, a clause of the
column's own, or the seat style over it.
Table shapes vary between pages of the same kind. Page 65's accessories table
carries trim mark columns and page 6's does not; a model added before its pricing
is settled prints no price columns at all. A missing column is a fact about the
guide, so it reads as None rather than raising.
Some rows are shaded. A shaded run is a table header only when its text is bold: the last two rows of page 6's accessories table are shaded body rows, and reading them as headers truncates the table they belong to.
Fixtures
packages/ai_access_audi/tests/data/order_guides/parser/fixtures/, one directory per
guide named audi_<market>_<model_year>_<edition_date>. See the README.md there for
what each file holds and what is deliberately absent.
Each guide's PDF ships in its own fixture directory, so the suite tests the parser
against the document it is read from. They are 5–7 MB apiece, which is why the repo's
large-file gate excludes that directory. conftest.py also searches the working tree and
its parents, and reports a missing PDF before the run and again at the end.
Rebuild a fixture's manifest from its PDF with:
python packages/ai_access_audi/tests/order_guides/build_fixture.py <pdf> <fixture_dir> <shipped_filename>