Skip to main content

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 statesHow it is recognizedSpansWhere it is read
a compatibility qualifieranything the three below are not755qualifier and rules on the record it follows
a market restrictionthe sentence Not Available for Vehicles sold in Alaska and Hawaii42PortInstalledAccessory.market_restriction
a model, trim or accessory closed to orderingNO LONGER AVAILABLE FOR ORDERING, NOT AVAILABLE FOR ORDERING, NOT AVAILABLE in any case72ContentsEntry.unavailable_note, and unavailable_stamped_pages in each fixture's manifest
an editorial note to the readeropens with *6OptionGroup.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.

SectionContents
CoverAudi of America, Model Year 2027, Order Guide, Invoice & Retail, the edition date
NoticesSignature Care, a tariff and pricing note — set in Aptos, not AudiType
Table of Contentsone 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 pagethe header band, the STANDARD FEATURES table banded by trim, and the OPTIONS table
Port Installed Accessoriesa code grid, then a description / code / retail / invoice table
Exterior & Interioran exterior-color × seat-trim availability matrix, INTERIOR COMPONENT OPTIONS, and inlay and wheel images with text captions
Audi exclusivea section cover, an ordering FAQ, then per-model special paint and tailored interior tables
Addendumthe low-aspect-ratio tire note
Updatesmodel / 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. X and O mean offered, S standard, a dash not offered. Nothing else appears in either guide; lowercase x occurs 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 against MA that 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:

TokenWhereWhat it is
$-600 / $-5652026 p28, code CRTa package discount, signed rather than bracketed
$10002027 p8, code U43printed without its thousands comma
$1.2952027 p31, S8 destinationprinted 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:

ApproachResult
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>