= 1, the price-range cross-field rule has been * enforced, etc. — so the implementation does not need to * re-check those invariants. It IS responsible for enforcing * provider-specific constraints (per-provider page-size caps, * supported sort tokens, locale restrictions) and raising * {@see ProviderRequestException} when the supplied query * violates them. * * The returned {@see SearchResult} MUST: * * - Carry every snapshot tagged with this provider's key * ({@see ProviderInterface::key()}). The §7.3 cross-check * is enforced by the {@see SearchResult} constructor; a * mismatched snapshot is a programming error inside this * provider implementation. * * - Reflect the {@see SearchQuery::page()} and * {@see SearchQuery::perPage()} values from the request * unless the provider had to clamp them — in which case the * {@see SearchResult} MUST reflect the actual values used. * * - Provide a non-null `hasMore` answer regardless of whether * `total` is populated. Per §7.2 `hasMore` is the canonical * pagination signal; consuming surfaces MUST NOT infer it * from `count($items) < $perPage`. * * - Forward provider-native diagnostic data (request id, * rate-limit headers, total-pages count) on `rawMeta` per * §7.2 — but consuming surfaces MUST NOT branch on its * contents. * * Zero matches is a normal-path outcome: the implementation * returns a {@see SearchResult} whose `items` list is empty and * whose `total` reflects the upstream's reported count (or null * when the upstream does not expose one). The implementation * MUST NOT raise an exception for this case. * * @param SearchQuery $query Normalized search query. * * @return SearchResult The matched snapshots together with the * pagination context for follow-up calls. * * @throws ProviderRequestException When `$query` violates * a provider-native input * constraint (per-provider * page-size cap exceeded, * unsupported sort token, * locale not served). * @throws AuthenticationException When the upstream * rejected the credentials * attached to the request. * @throws RateLimitExceededException When the upstream is * currently refusing * requests due to a * rate-limit window. * @throws TemporaryUnavailableException When the upstream * returned a transient * 5xx-style failure or * the request timed out * at the network layer. * @throws ProviderResponseException When the upstream * response failed to * parse into the * cross-provider model * or violated the §7.4 * contract. * @throws UnsupportedFeatureException When the dispatch layer * routes a search request * to a provider that does * not support search; in * practice the dispatch * layer should have * consulted * {@see ProviderCapabilities::supportsSearch()} * before reaching this * method. * @throws ProviderException When any other §10.2 * error mode applies. */ public function searchProducts( SearchQuery $query ): SearchResult; }