Portal
Sign In Console

PLO Tag Query & Browsing Contract

How to query the PLO range/tag browser and what comes back. Applies to plo4, plo5, and plo6 hand requests (examples use plo4). The controls described here are additive: they never mutate the underlying canonical bucket enumeration, they only filter, sort, page, and annotate it.

The request carries two optional objects:

ObjectPurpose
ploTagOptions (alias plo_tag_options)Response shaping: schema, per-combo tag/facet payloads, paging, sorting, zero-row skipping, profiling, strategy toggle.
plo_tag_queryFiltering + query semantics: require / exclude tags, comboContains, rankPattern, groupBy, plus its own paging/sort/skip that can override ploTagOptions.

Both are optional. Sending neither returns the default first page of the canonical bucket map (with strategy — see the note below). Sending plo_tag_query (even empty) switches the response into "query mode" and adds a ploTagQueryResult block.

Every field accepts both camelCase and snake_case. Where a field is read from both objects, the precedence rules in Field precedence apply.

Strategy runs by default. The PLO grid pipeline runs model inference unless you turn it off. A bare browse request therefore returns ploStrategy and needs a configured model (otherwise it errors with "V2 model not configured"). To browse tags/buckets without inference, set getStrategy: false (or ploGetStrategy: false) — see Strategy inference toggle.


Quick reference

plo_tag_query

FieldTypeDefaultMeaning
requirestring or string[][]Tag names/aliases the hand must have (AND).
excludestring or string[][]Tag names/aliases the hand must not have.
comboContains (combo_contains)string or string[][]Suit-specific card filter (AND). See comboContains.
rankPattern (rank_pattern)stringunsetContained rank-multiset filter (1–4 ranks, any suit/order). See rankPattern.
groupBy (group_by)string or string[][]Grouping key(s); echoed back (grouping itself is reserved).
normalizestring"conditional"Echoed normalization mode.
pageOffset (page_offset)int0Page start.
pageSize (page_size)int or "all"200Page size; integer capped at 5000; "all" returns the full matched set (no cap).
sortField (sort_field)string""Sort key. See Sorting.
sortActionSlot (sort_action_slot)int-1Action column for pct/ev (and the legacy skip criterion).
sortDir (sort_dir)string"desc""desc" or "asc".
skipZeroPages (skip_zero_pages)boolfalseDrop zero/unavailable rows before paging. See Skipping zero rows.
skipZeroSlot (skip_zero_slot)int-1Sort-independent skip criterion: drop rows with zero prob on this action slot.
skipZeroCollapsedRaise (skip_zero_collapsed_raise)boolfalseSort-independent skip criterion: drop rows with zero collapsed raise mass.
max_groupsint0Reserved (grouping cap).
max_examples_per_groupint0Reserved (grouping cap).

ploTagOptions

FieldTypeDefaultMeaning
includeSchema (include_schema)boolfalseAdd ploTagSchema (the tag dictionary for this variant/street).
includePerComboTags (include_per_combo_tags)boolfalseAdd ploTagMap (tag-id list per combo).
includePerComboFacets (include_per_combo_facets)boolfalseAdd ploFacets (named facet values per combo).
getStrategy / includeStrategy (get_strategy / include_strategy)booltrueRun model inference. Off ⇒ pure taxonomy/tag browse, no ploStrategy.
profileboolfalseAdd ploProfile timing/count block.
pageOffset (page_offset)int0Page start.
pageSize (page_size)int or "all"200Page size; integer capped at 5000; "all" = full matched set (no cap).
aggregateAll (aggregate_all)boolfalseCompute exact aggregate over the full matched set, then strip per-combo payload.
sortField (sort_field)string""Same values as plo_tag_query.sortField.
sortActionSlot (sort_action_slot)int-1Action column for pct/ev.
sortDir (sort_dir)string"desc""desc" or "asc".
skipZeroPages (skip_zero_pages)boolfalseDrop zero/unavailable rows before paging. See Skipping zero rows.
skipZeroSlot (skip_zero_slot)int-1Sort-independent skip criterion (single action slot).
skipZeroCollapsedRaise (skip_zero_collapsed_raise)boolfalseSort-independent skip criterion (collapsed raise mass).

comboContains, rankPattern, require, exclude, and groupBy live only in plo_tag_query. ploTagOptions does not carry them. The paging, sort, and skip-zero fields live in both.

Strategy inference toggle

Tag/bucket browsing is logically separable from inference, but the server runs strategy by default. Strategy outputs (ploStrategy, ploReach, actionLabels, and the pct/ev/raise/raiseEv sorts and skipZeroPages filtering) all depend on inference.

Turn inference off (pure browse — no ploStrategy, no model required) with any one of:

  • top level: ploGetStrategy: false / plo_get_strategy / ploRunStrategy / plo_run_strategy
  • inside ploTagOptions: getStrategy: false / get_strategy / includeStrategy / include_strategy

Leaving all of them unset ⇒ inference on. The top-level toggles win over the ploTagOptions toggles.


comboContains

comboContains filters the range down to hands that physically contain specific, suit-qualified cards. It is the right tool for "show me every hand that has the Ace of hearts" or "show me hands with both the Ah and the Kh". For a contained set of ranks in any suit (e.g. "any A-K-8-3 hand", "any queen pair") use rankPattern; broad class searches (e.g. "any pair", "double suited") belong in require / exclude.

Value format

Each entry is a string of one or more concatenated 2-character cards:

  • Rank (first char): 2 3 4 5 6 7 8 9 T J Q K A, case-insensitive.
  • Suit (second char): c d h s, case-insensitive.
  • Normalized internally to uppercase rank + lowercase suit (ah -> Ah).
"comboContains": ["Ah"]            // single card
"comboContains": ["AhKh"]          // compact multi-card string
"comboContains": ["Ah", "Kh"]      // array form, same as "AhKh"
"comboContains": ["AhKsQdJc"]      // a fully specified 4-card hand (plo4)

A value may be a single string or an array of strings. Multiple cards (whether in one compact string or spread across the array) are combined and de-duplicated, then matched with AND semantics: a hand must contain every requested card.

What it returns

Unlike normal browsing (which returns canonical, suit-isomorphic representatives with a multiplicity), comboContains returns the actual physical combos that contain the requested cards. Every returned row therefore has multiplicity = 1, and:

totalMatchingHandCount == totalMatchingComboCount

Each combo is still mapped to its bucket in ploBucketMap, so bucketing, tags, facets, sorting, and strategy all work on the physical rows.

Exact, deterministic match counts (plo4 preflop, empty board):

QueryMatched handsWhy
["Ah"]20825choose the other 3 cards from 51: C(51,3)
["AhKh"]1225choose the other 2 from 50: C(50,2)
["AhKsQdJc"]1the hand is fully specified

Postflop, the board cards are removed from the deck first, so the counts shrink accordingly.

Validation (fails closed)

  • A value must have even length and every 2-char chunk must be a valid card. Anything else (e.g. "AK83", "ZZ", "Ah5") marks that value invalid.
  • If any value is invalid, the whole query fails closed: 0 matches, with:
"ploTagQueryResult": {
  "status": "invalid_combo_contains",
  "warning": "one_or_more_combo_contains_cards_are_invalid",
  "comboContains": ["Ah", "ZZ"]
}
  • Requesting more distinct cards than the hand holds (e.g. 5 cards for plo4) yields 0 matches with status: "ok".
  • Requesting a card that is on the board yields 0 matches (you cannot hold a board card).

Composing with tags and sort

comboContains composes with require / exclude (the tag filter is applied to each physical combo) and with any sortField:

"plo_tag_query": {
  "comboContains": ["Ah"],
  "require": ["double_suited"],
  "exclude": ["pair"],
  "sortField": "raise",
  "sortDir": "desc",
  "pageOffset": 0,
  "pageSize": 50
}

rankPattern

rankPattern filters the range down to hands whose ranks contain a given rank multiset, ignoring suits and order. It is the rank-only complement to comboContains: use comboContains when you care about specific suited cards (Ah), and rankPattern when you care only about ranks the hand must hold ("every hand with a queen pair", "every A-K-8-3 hand").

This is a contained (subset) match, not exact equality, and the pattern may be shorter than the hand (1–4 ranks). This changed from the older "exactly holeCardCount ranks, exact multiset" contract. For plo4 a full 4-rank pattern still behaves like the old exact filter (a 4-rank subset of a 4-card hand is the whole hand), so existing plo4 callers using full patterns are unaffected.

Value format

A single string of 1 to min(holeCardCount, 4) rank characters (so 1–4 ranks for plo4; still capped at 4 ranks for plo5/plo6), no suits:

  • Rank: 2 3 4 5 6 7 8 9 T J Q K A, case-insensitive.
  • Order does not matter; duplicate ranks are allowed (and meaningful).
  • Echoed back uppercased, in the order sent (not reordered).
"rankPattern": "QQ"     // any hand holding at least a pair of queens
"rankPattern": "AK"     // any hand holding at least one ace and one king
"rankPattern": "AK83"   // plo4: hands whose ranks are exactly {A,K,8,3}
"rankPattern": "3k8a"   // same as "AK83" (case- and order-insensitive)
"rankPattern": "AAK3"   // hands holding {A,A,K,3} — two aces, a K and a 3

Unlike comboContains, rankPattern is a single string (not an array) and carries no suits. comboContains: "AK83" is still invalid exact-card input and fails closed as invalid_combo_contains — the two filters do not overlap.

Semantics

The hand's hole-card ranks are sorted and tested for multiset inclusion of the sorted pattern: the hand matches iff its ranks contain every rank in the pattern, counting duplicates. Because duplicate ranks are significant, QQ matches only hands holding two or more queens, and AAK3 matches only hands holding two or more aces plus a K and a 3.

For a plo4 hand (4 ranks):

  • a 1–3-rank pattern is a genuine contains query (the remaining hole cards are unconstrained), e.g. QQ = "holds a queen pair, plus anything";
  • a 4-rank pattern fills the whole hand, so it collapses to exact equality, e.g. AK83 = "the four ranks are exactly A, K, 8, 3".

For plo5/plo6, even a 4-rank pattern leaves the extra hole card(s) free, so it stays a partial (contains) match.

What it returns

On its own, rankPattern browses like the default: it returns canonical, suit-isomorphic representatives with their multiplicity, so totalMatchingComboCount is the raw-combo total and ploMultiplicity values can be > 1. Combined with comboContains, it returns the physical combos that contain the requested cards (multiplicity = 1), exactly as comboContains does on its own.

Exact, deterministic raw-combo counts (plo4 preflop, empty board):

QuerytotalMatchingComboCountWhy
rankPattern: "AK83"2564 distinct ranks × 4 suits each: 4^4
rankPattern: "AAK3"96aces C(4,2)=6, K 4, 3 4: 6·4·4
rankPattern: "AA"6961hands with ≥2 aces: C(4,2)·C(48,2)+C(4,3)·48+1
rankPattern: "AK83" + comboContains: ["Ah"]64Ah fixed, other 3 ranks × 4 suits: 4^3

Postflop the board cards are removed from the deck first, so counts shrink accordingly (a rank fully consumed by the board can drop the match to 0).

Validation (fails closed)

The pattern must be 1 to min(holeCardCount, 4) characters and every character a valid rank. Anything else — empty string, too long (e.g. "AK835" for plo4, or any 5+-rank pattern), or an invalid rank character ("AK8Z") — fails closed: 0 matches, with:

"ploTagQueryResult": {
  "status": "invalid_rank_pattern",
  "warning": "rank_pattern_is_invalid",
  "rankPattern": "AK8Z"
}

If both comboContains and rankPattern are invalid in the same request, invalid_combo_contains takes precedence.

Composing with comboContains, tags, and sort

rankPattern ANDs with comboContains, require / exclude, and any sortField:

"plo_tag_query": {
  "rankPattern": "AK83",
  "comboContains": ["Ah"],
  "sortField": "raise",
  "sortDir": "desc"
}

This returns the A-K-8-3 hands that hold the Ah, ordered by collapsed bet/raise probability.


Tag filtering: require / exclude

require and exclude take tag names or aliases and combine as "has all of require AND none of exclude". The available vocabulary is data-driven and depends on the variant and street, so treat the schema as the source of truth rather than hard-coding names:

  • Set ploTagOptions.includeSchema: true to get ploTagSchema, an array of { id, name, plane, domain, aliases } entries valid for the current holeCardCount and board street.
  • Use a name or any of its aliases in require / exclude.

If a require tag cannot be resolved the query fails closed (status: "unresolved_required_tag"); an unresolved exclude tag is ignored with status: "ok_with_unresolved_exclude". Unresolved aliases are listed under ploTagUnresolved (with ploTagWarning: "unresolved_tag_alias").


Sorting

Set sortField (and sortDir, default "desc"). Pages are slices of one global order, so paging through a sort is stable.

sortFieldNeeds strategy?sortActionSlot?Ranks by
"combos"nonosuit-isomorphic multiplicity
"pct"yesrequiredprobability of the chosen action slot
"ev"yesrequiredEV of the chosen action slot
"raise" / "betRaise" / "bet_raise" / "betraise"yesnocollapsed bet/raise = sum(strategy[2..10])
"raiseEv" / "raise_ev"yesnosum(strategy[s] * evs[s]) / sum(strategy[s]), s=2..10

Action slots: 0 = fold, 1 = check/call, 2..10 = all-in + bet/raise sizes.

  • "combos" is resolved before inference from multiplicity, so it is applied directly (ploPaging.sort.applied: true).
  • "pct" / "ev" / "raise" / "raiseEv" depend on model output, so the full matched set is ranked host-side by that output and sliced to the requested page. In the returned response the final ploPaging.sort.applied is true and ploPageOrder reflects the order. Combos with no inference output always sort to the end regardless of direction.
  • For "ev", if the model emits no usable or non-identical EVs the sort is not applied and sort.applied: false with sort.reason: "ev_unavailable" (the matched set is still paged, not returned whole).
  • An unsupported sortField keeps normal paging and sets sort.applied: false with sort.warning: "unsupported_sort_field".
  • If a deployment disables host-side inference sorting, the requested page is returned unsorted with sort.applied: false and sort.reason: "server_side_sort_disabled".

An inference sort (pct/ev/raise/raiseEv) with strategy turned off, or a pct/ev sort without a valid sortActionSlot, is a hard error — see Errors.


Skipping zero rows

skipZeroPages: true drops rows the hand never plays before paging, so a page is not padded with zero-probability combos. It requires strategy inference. The rows it drops are chosen by a criterion, resolved in this order:

  1. skipZeroCollapsedRaise: true — drop combos whose collapsed raise mass sum(strategy[2..10]) is zero. Sort-independent (works with any sort, including "combos", and needs no sortActionSlot). Wins if both explicit criteria are set.
  2. skipZeroSlot: <0..10> — drop combos with zero probability on that single action slot. Also sort-independent.
  3. Legacy fallback (neither explicit criterion supplied): raise / raiseEv / empty sort fields fall back to collapsed raise mass; other fields fall back to sortActionSlot.

If none of these resolves to a criterion, skipZeroPages: true is a hard error (see Errors).

Interaction with the sort field:

  • For pct/raise, it drops zero/unavailable rows before paging.
  • For raiseEv, it drops rows with no aggregate raise weight but keeps finite aggregate EV values of 0.0.
  • For ev, the sort itself never treats 0.0 as unavailable (0 is a valid EV), but an explicit skipZeroSlot / skipZeroCollapsedRaise still filters by action probability.

When skipZeroPages prunes rows, the response's totalMatchingHandCount / totalMatchingComboCount (and the ploTagQueryResult matched* counts) are recomputed over the surviving set.


Paging

  • pageOffset / pageSize page the matched set. Integer pageSize is capped at 5000 (default 200).
  • pageSize: "all" returns the entire matched set with per-combo data (no cap). The full unfiltered PLO set is large, so this is an explicit opt-in.
  • aggregateAll: true enumerates the full matched set for an exact aggregate and then strips the per-combo payload, so only the rollup (ploOverallAggregate, bucket-tree aggregates, aggregateAll: true) is returned.
  • Response paging echoes pageOffset, pageSize, pageHandCount, totalMatchingHandCount, totalMatchingComboCount, hasNextPage, nextOffset.

Field precedence

When both objects are present:

  • plo_tag_query supplies require, exclude, groupBy, comboContains, rankPattern, and (always) its pageOffset / pageSize.
  • skipZeroPages, skipZeroSlot, and skipZeroCollapsedRaise from plo_tag_query each win only if that field is present, otherwise the ploTagOptions value survives.
  • A sort from plo_tag_query wins only if sortField is non-empty, so a ploTagOptions-level sort survives a filter-only plo_tag_query. (As a special case, a plo_tag_query that carries skipZeroPages and sortActionSlot but no sortField still adopts that sortActionSlot.)

Errors

Some misconfigured requests return a plain error object ({ "success": false, "error": "..." }) instead of a taxonomy/query response:

ConditionError message (substring)
Inference sort (pct/ev/raise/raiseEv) requested with strategy offsortField "<f>" requires strategy inference; set ploTagOptions.getStrategy=true
pct / ev sort without a valid sortActionSlot in [0,10]sortField "<f>" requires sortActionSlot in [0, 10]
skipZeroPages requested with strategy offskipZeroPages requires strategy inference; set ploTagOptions.getStrategy=true
skipZeroPages with no resolvable criterionskipZeroPages requires skipZeroSlot in [0, 10], skipZeroCollapsedRaise, or sortActionSlot
Strategy on but no model configured for the spotV2 model not configured (or the selector's reason)

Filter-input problems (invalid_combo_contains, invalid_rank_pattern, unresolved_required_tag) are not errors — they fail closed to 0 matches inside ploTagQueryResult.status (see the relevant sections).


Response

Always present (taxonomy)

FieldMeaning
taxonomyOnlytrue.
holeCardCount4, 5, or 6.
numBoardCardsboard size (0 preflop).
sampledModefalse (results are exact).
ploBucketTree3-level taxonomy tree (id, label, children).
ploBucketMap{ comboString: bucketKey } for the page. Combo strings are concatenated cards, e.g. "AhKdQcJs".
ploMultiplicity{ comboString: suitIsomorphicCount } (always 1 for comboContains).
ploBucketCounts{ bucketKey: matchedCount } over the full matched set.
ploPagingpaging block + sort echo (see below).
ploPageOrderarray of combo strings in global sort order (present when a sort is applied).
fullTotalCount, totalCanonicalHandCount, totalRawComboCountenumeration totals.
model, modelMd5resolved model + its md5 (present when strategy ran).
computeTimeMs, stageTimingsMsrequest timing / per-stage breakdown (see Timing).

ploPaging.sort echoes the request and the resolution state. field and dir are always present; applied says whether the page already reflects the global order; actionSlot appears for pct/ev; skipZeroPages, skipZeroSlot, skipZeroCollapsedRaise are echoed when honored; warning/reason appear on the unsupported / ev-degenerate / sort-disabled paths. While a sort is still deferred (applied: false) the object also carries requestedPageOffset / requestedPageSize; those are removed once the page has been sliced (applied: true).

"sort": {
  "field": "raise",
  "dir": "desc",
  "applied": true
}

When plo_tag_query is present: ploTagQueryResult

"ploTagQueryResult": {
  "schemaVersion": 1,
  "sampledMode": false,
  "estimated": false,
  "available": true,
  "status": "ok",
  "normalization": "conditional",
  "require": ["double_suited"],
  "exclude": ["pair"],
  "groupBy": [],
  "comboContains": ["Ah"],
  "matchedHandCount": 8479,
  "matchedComboCount": 8479,
  "matchedWeight": 8479,
  "paging": { "...": "mirror of ploPaging" },
  "groups": []
}

status values: ok, invalid_combo_contains, invalid_rank_pattern, unresolved_required_tag, ok_with_unresolved_exclude, tag_data_unavailable. rankPattern (uppercased) is echoed only when a rank pattern was requested; skipZeroPages: true is echoed when honored. (groups is reserved and returned empty by this path.)

When tag payloads are requested

  • includeSchema -> ploTagSchema: [{ id, name, plane, domain, aliases }].
  • includePerComboTags -> ploTagMap: { comboString: [tagId, ...] } (resolve ids via ploTagSchema).
  • includePerComboFacets -> ploFacets: { comboString: { facetName: value } }.
  • ploTagAvailable, ploTagSchemaVersion, and on alias problems ploTagWarning + ploTagUnresolved.

When strategy inference is on

FieldMeaning
ploStrategyper combo: strategy[11], evs[11], ev, raise, betRaise, and reach (if a reach query ran).
ploOverallAggregatemultiplicity-weighted fold/call/raise + EV rollup.
ploBucketTree[].aggregatethe same rollup attached per tree node.
ploReach{ comboString: reachProbability } (when reach is computed).
actionLabels, actionHistory, tableState, supportScoregrid/UI metadata.

A single ploStrategy entry:

"AhKhQdJd": {
  "strategy": [0.12, 0.35, 0.03, 0.10, 0.18, 0.08, 0.14, 0.0, 0.0, 0.0, 0.0],
  "raise": 0.53,
  "betRaise": 0.53,
  "ev": 1.24,
  "evs": [0.0, 1.1, 1.2, 1.3, 1.4, 1.2, 1.1, 0.0, 0.0, 0.0, 0.0]
}

If show_labels: true, each ploStrategy entry also gets ev_ranks[11] and action_qualities[11] alongside strategy[11] / evs[11]. Setting reduced_actions_tier adds a per-bucket collapsed-action view.

Reach query: reach_queryreachResult

Send a reach_query object to get a bucketed reach rollup (which hands reach this node) in addition to per-combo reach:

reach_query fieldTypeDefaultMeaning
target_seat_noint-1Seat whose range to compute (-1 = actor).
normalizestring"conditional""conditional" or "absolute".
group_bystring"bucket""bucket" or "combo".
max_bucketsint0Cap on buckets returned (0 = all).
max_examples_per_bucketint0Cap on examples per bucket (0 = all).

The response adds reachResult (bucketed/normalized/truncated reach). A reach query computes reach over the full matched set.

Pending deal

If the request needs board cards that were not supplied, the pipeline returns early with pendingDeal set to "flop" / "turn" / "river" (plus tableState / actionHistory context) instead of a combo page, so the frontend can prompt for the missing board.

Timing and profiling

  • computeTimeMs — total request time (ms).
  • stageTimingsMs{ state_enumerate_ms, obs_build_ms, inference_ms, reach_ms, reach_scope, emit_scope, aggregate_ms, num_combos } (cacheHit is added when the full-set result cache is engaged). reach_scope is "full" / "page" / "sampled" / "skipped"; emit_scope is "full" / "page".
  • profile: true (in ploTagOptions) adds ploProfile with filterPageMs, bucketCount, enumeratedCanonicalHands, matchedCanonicalHands, returnedPageHands.

Worked examples

1. Browse the default first page, no inference

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": { "getStrategy": false, "pageOffset": 0, "pageSize": 50 }
}

Returns the first 50 canonical buckets/combos with ploMultiplicity and no ploStrategy. (Omitting getStrategy: false runs the model and adds ploStrategy.)

2. comboContains: every hand with the Ah

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": { "pageOffset": 0, "pageSize": 100 },
  "plo_tag_query": { "comboContains": ["Ah"] }
}

ploTagQueryResult.matchedHandCount == 20825; every key in ploBucketMap contains Ah; all ploMultiplicity values are 1.

3. comboContains + tags + collapsed raise sort (with inference)

{
  "protocol_ver": "v2.1",
  "show_labels": true,
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": {
    "getStrategy": true,
    "pageOffset": 0,
    "pageSize": 50,
    "sortField": "raise",
    "sortDir": "desc",
    "includePerComboTags": true
  },
  "plo_tag_query": {
    "require": ["double_suited"],
    "exclude": ["pair"],
    "comboContains": ["Ah"],
    "sortField": "raise",
    "sortDir": "desc"
  }
}

Returns double-suited, unpaired hands that contain Ah, ordered by total bet/raise probability; ploPageOrder holds the order and each ploStrategy row carries raise/betRaise.

4. pct sort on one size, skipping zero rows

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": {
    "getStrategy": true,
    "pageOffset": 0,
    "pageSize": 50,
    "sortField": "pct",
    "sortActionSlot": 4,
    "sortDir": "desc",
    "skipZeroPages": true
  }
}

Ranks combos by the probability of action slot 4 (a bet/raise size). Combos with no probability there sort to the end, and skipZeroPages (legacy fallback → sortActionSlot) removes those zero rows before paging.

5. Skip zero rows independent of the sort

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "ploTagOptions": {
    "getStrategy": true,
    "sortField": "combos",
    "skipZeroPages": true,
    "skipZeroCollapsedRaise": true,
    "pageSize": 100
  }
}

Sorts by suit-isomorphic multiplicity (sort.applied: true) but still hides every combo whose collapsed raise mass sum(strategy[2..10]) is zero. skipZeroCollapsedRaise (and skipZeroSlot) work with any sort and need no sortActionSlot.

6. rankPattern: any queen pair, and every A-K-8-3 hand

{
  "protocol_ver": "v2.1",
  "hand": { "game_type": "plo4", "...": "..." },
  "plo_tag_query": { "rankPattern": "QQ", "pageSize": "all" }
}

ploTagQueryResult.status == "ok", rankPattern == "QQ", and every key in ploBucketMap holds at least two queens. Using "AK83" instead returns the exact-{A,K,8,3} hands (totalMatchingComboCount == 256); adding "comboContains": ["Ah"] narrows it to the 64 physical combos that hold the ace of hearts (each with multiplicity = 1).