--- /mnt/data/rx_pristine/reconcile_20260815_0/src/Provider/Xtree/XtreeJsonApiClient.php	2026-08-10 09:30:35.000000000 +0000
+++ src/Provider/Xtree/XtreeJsonApiClient.php	2026-08-15 18:42:01.719767419 +0000
@@ -25,7 +25,8 @@
         string $vocabularyUri,
         string $searchTerm,
         string $language = 'all',
-        int $jsonFull = 1
+        int $jsonFull = 1,
+        ?string $qualifier = null,
     ): array {
         $this->ensureLoggedIn();
 
@@ -40,7 +41,7 @@
                 'restrictedto' => 'Restrict to IDs',
                 'jsonfull' => (string)$jsonFull,
                 'lang' => $language,
-                'homonymlexicalvalue' => '',
+                'homonymlexicalvalue' => $qualifier ?? '',
                 'submit' => 'getSearchVocItemsByTerm',
             ]);
 /*
@@ -67,7 +68,8 @@
         string $nodeId,
         string $searchTerm,
         string $language = 'all',
-        int $jsonFull = 1
+        int $jsonFull = 1,
+        ?string $qualifier = null,
     ): array {
         $this->ensureLoggedIn();
 
@@ -83,6 +85,7 @@
                 'jsonfull' => (string)$jsonFull,
                 'lang' => $language,
                 'submit' => 'getFetchHierarchy',
+                'homonymlexicalvalue' => $qualifier ?? '',
             ]);
 
         return $this->getJson($url, 'getFetchHierarchy');
--- /mnt/data/rx_pristine/reconcile_20260815_0/src/Infrastructure/Discovery/Adapter/XtreeJsonApiAdapter.php	2026-08-08 06:04:28.000000000 +0000
+++ src/Infrastructure/Discovery/Adapter/XtreeJsonApiAdapter.php	2026-08-15 18:42:40.687179932 +0000
@@ -6,6 +6,7 @@
 use App\Application\Reconciliation\DiscoveredCandidate;
 use App\Application\Reconciliation\ReconciliationCommand;
 use App\Auth\TenantContext;
+use App\Domain\Reconciliation\ContextTypeUri;
 use App\Infrastructure\Discovery\Audit\DiscoveryAccessAuditContext;
 use App\Infrastructure\Discovery\Audit\DiscoveryAccessAuditEntry;
 use App\Infrastructure\Discovery\Audit\DiscoveryAccessAuditLogger;
@@ -74,6 +75,7 @@
         }
 
         $term = trim($command->sourceValue);
+        $qualifier = $this->qualifierFromContext($command->contextProperties);
         $language = $command->language ?? 'all';
         $operation = $this->scopeVocabularyUri === $this->targetVocabularyUri
             ? 'getSearchVocItemsByTerm'
@@ -88,6 +90,7 @@
                     $term,
                     $language,
                     1,
+                    $qualifier,
                 );
             } else {
                 $path = (string) parse_url($this->scopeVocabularyUri, PHP_URL_PATH);
@@ -101,6 +104,7 @@
                     $term,
                     $language,
                     1,
+                    $qualifier,
                 );
             }
 
@@ -180,6 +184,26 @@
         }
     }
 
+
+    /** @param array<int|string, mixed> $properties */
+    private function qualifierFromContext(array $properties): ?string
+    {
+        foreach ($properties as $property) {
+            if (!is_array($property)) {
+                continue;
+            }
+
+            if (trim((string)($property['pid'] ?? '')) !== ContextTypeUri::QUALIFIER) {
+                continue;
+            }
+
+            $value = trim((string)($property['v'] ?? ''));
+            return $value !== '' ? $value : null;
+        }
+
+        return null;
+    }
+
     private function elapsedMs(int $startedAt): int
     {
         return max(0, (int) round((hrtime(true) - $startedAt) / 1_000_000));
--- /mnt/data/rx_pristine/reconcile_20260815_0/src/Provider/LocalStore/ReconciliationStoreProvider.php	2026-06-29 18:55:10.000000000 +0000
+++ src/Provider/LocalStore/ReconciliationStoreProvider.php	2026-08-15 18:42:40.687961498 +0000
@@ -1,176 +1,202 @@
-<?php
-
-declare(strict_types=1);
-
-namespace App\Provider\LocalStore;
-
-use App\Auth\TenantContext;
-use App\Model\Candidate;
-use App\Model\ReconciliationQuery;
-use App\Provider\ProviderInterface;
-use App\ReconciliationStore\ReconciliationStore;
-use App\Util\TextNormalizer;
-use App\ReconciliationStore\QueryTokenizer;
-
-final class ReconciliationStoreProvider implements ProviderInterface
-{
-    public function key(): string
-    {
-        return 'reconciliation-store';
-    }
-
-    public function search(ReconciliationQuery $query, TenantContext $tenant, array $vocabularyConfig): array
-    {
-        $store = $vocabularyConfig['store'] ?? null;
-
-
-
-        if (!$store instanceof ReconciliationStore || !$store->enabled) {
-            return [];
-        }
-
-        $basePath = dirname(__DIR__, 3) . '/' . trim($store->targetPath, '/\\');
-        $termsPath = $basePath . '/terms.json';
-
-        if (!is_file($termsPath)) {
-            return [];
-        }
-
-        $terms = json_decode((string)file_get_contents($termsPath), true);
-
-
-        if (!is_array($terms)) {
-            return [];
-        }
-
-        $termKey = TextNormalizer::normalize($query->term);
-        $hits = $terms[$termKey] ?? [];
-
-        if ($hits === []) {
-            $hits = $this->findTokenHits($query->term, $terms);
-        }
-
-
-
-
-
-        $candidates = [];
-
-        foreach ($hits as $hit) {
-            $score = (int)round(100 * (float)($hit['weight'] ?? 1));
-            $qualifier = empty($hit['qualifier']) ? null : ' (' . $hit['qualifier'] . ')';
-            $candidates[] = new Candidate(
-                id: (string)$hit['id'],
-                label: ((string)$hit['label'] ?? null). $qualifier,
-                score: $score,
-                match: $score >= 100,
-                meta: [
-                    'provider' => $this->key(),
-                    'store' => $store->id,
-                    'role' => $hit['role'] ?? null,
-                    'lang' => $hit['lang'] ?? null,
-                    'qualifier' => $hit['qualifier'] ?? null,
-                    'record_type' => $hit['record_type'] ?? null,
-                ],
-            );
-        }
-
-        return array_slice($candidates, 0, $query->limit);
-    }
-/*
-    private function findTokenHits(string $queryTerm, array $terms): array
-    {
-        $tokens = (new QueryTokenizer())->tokenize($queryTerm);
-
-        if ($tokens === []) {
-            return [];
-        }
-
-        $grouped = [];
-
-        foreach ($tokens as $token) {
-            foreach ($terms[$token] ?? [] as $hit) {
-                $id = (string)($hit['id'] ?? '');
-
-                if ($id === '') {
-                    continue;
-                }
-
-                if (!isset($grouped[$id])) {
-                    $hit['weight'] = ((float)($hit['weight'] ?? 1)) * 0.72;
-                    $hit['_token_matches'] = 0;
-                    $grouped[$id] = $hit;
-                }
-
-                $grouped[$id]['_token_matches']++;
-            }
-        }
-
-        $grouped = array_values($grouped);
-
-        usort($grouped, static function (array $a, array $b): int {
-            return ($b['_token_matches'] <=> $a['_token_matches'])
-                ?: (($b['weight'] ?? 0) <=> ($a['weight'] ?? 0));
-        });
-
-        return $grouped;
-    }
-    */
-
-    private function findTokenHits(string $queryTerm, array $terms): array
-    {
-        $tokens = (new QueryTokenizer())->analyze($queryTerm);
-
-        if ($tokens === []) {
-            return [];
-        }
-
-        $grouped = [];
-
-        foreach ($tokens as $tokenInfo) {
-            $token = $tokenInfo['token'];
-            $tokenWeight = (float)$tokenInfo['weight'];
-
-            foreach ($terms[$token] ?? [] as $hit) {
-                $id = (string)($hit['id'] ?? '');
-
-                if ($id === '') {
-                    continue;
-                }
-
-                $weightedHit = $hit;
-                $weightedHit['weight'] = ((float)($hit['weight'] ?? 1)) * $tokenWeight;
-                $weightedHit['_token_matches'] = 1;
-                $weightedHit['_token_origin'] = $tokenInfo['origin'];
-                $weightedHit['_matched_token'] = $token;
-
-                if (!isset($grouped[$id])) {
-                    $grouped[$id] = $weightedHit;
-                    continue;
-                }
-
-                $grouped[$id]['_token_matches']++;
-
-                if ($weightedHit['weight'] > ($grouped[$id]['weight'] ?? 0)) {
-                    $grouped[$id]['weight'] = $weightedHit['weight'];
-                    $grouped[$id]['_token_origin'] = $weightedHit['_token_origin'];
-                    $grouped[$id]['_matched_token'] = $weightedHit['_matched_token'];
-                }
-            }
-        }
-
-        $grouped = array_values($grouped);
-
-        usort($grouped, static function (array $a, array $b): int {
-            return ($b['_token_matches'] <=> $a['_token_matches'])
-                ?: (($b['weight'] ?? 0) <=> ($a['weight'] ?? 0));
-        });
-
-        return $grouped;
-    }
-
-    public function preview(string $id, TenantContext $tenant): string
-    {
-        return '';
-    }
+<?php
+
+declare(strict_types=1);
+
+namespace App\Provider\LocalStore;
+
+use App\Auth\TenantContext;
+use App\Domain\Reconciliation\ContextTypeUri;
+use App\Model\Candidate;
+use App\Model\ReconciliationQuery;
+use App\Provider\ProviderInterface;
+use App\ReconciliationStore\ReconciliationStore;
+use App\Util\TextNormalizer;
+use App\ReconciliationStore\QueryTokenizer;
+
+final class ReconciliationStoreProvider implements ProviderInterface
+{
+    public function key(): string
+    {
+        return 'reconciliation-store';
+    }
+
+    public function search(ReconciliationQuery $query, TenantContext $tenant, array $vocabularyConfig): array
+    {
+        $store = $vocabularyConfig['store'] ?? null;
+
+
+
+        if (!$store instanceof ReconciliationStore || !$store->enabled) {
+            return [];
+        }
+
+        $basePath = dirname(__DIR__, 3) . '/' . trim($store->targetPath, '/\\');
+        $termsPath = $basePath . '/terms.json';
+
+        if (!is_file($termsPath)) {
+            return [];
+        }
+
+        $terms = json_decode((string)file_get_contents($termsPath), true);
+
+
+        if (!is_array($terms)) {
+            return [];
+        }
+
+        $termKey = TextNormalizer::normalize($query->term);
+        $hits = $terms[$termKey] ?? [];
+
+        if ($hits === []) {
+            $hits = $this->findTokenHits($query->term, $terms);
+        }
+
+        $qualifier = $this->qualifierFromContext($query->context->properties);
+        if ($qualifier !== null) {
+            $normalizedQualifier = TextNormalizer::normalize($qualifier);
+            $hits = array_values(array_filter(
+                $hits,
+                static fn (array $hit): bool => TextNormalizer::normalize((string)($hit['qualifier'] ?? '')) === $normalizedQualifier,
+            ));
+        }
+
+        $candidates = [];
+
+        foreach ($hits as $hit) {
+            $score = (int)round(100 * (float)($hit['weight'] ?? 1));
+            $qualifier = empty($hit['qualifier']) ? null : ' (' . $hit['qualifier'] . ')';
+            $candidates[] = new Candidate(
+                id: (string)$hit['id'],
+                label: ((string)$hit['label'] ?? null). $qualifier,
+                score: $score,
+                match: $score >= 100,
+                meta: [
+                    'provider' => $this->key(),
+                    'store' => $store->id,
+                    'role' => $hit['role'] ?? null,
+                    'lang' => $hit['lang'] ?? null,
+                    'qualifier' => $hit['qualifier'] ?? null,
+                    'record_type' => $hit['record_type'] ?? null,
+                ],
+            );
+        }
+
+        return array_slice($candidates, 0, $query->limit);
+    }
+/*
+    private function findTokenHits(string $queryTerm, array $terms): array
+    {
+        $tokens = (new QueryTokenizer())->tokenize($queryTerm);
+
+        if ($tokens === []) {
+            return [];
+        }
+
+        $grouped = [];
+
+        foreach ($tokens as $token) {
+            foreach ($terms[$token] ?? [] as $hit) {
+                $id = (string)($hit['id'] ?? '');
+
+                if ($id === '') {
+                    continue;
+                }
+
+                if (!isset($grouped[$id])) {
+                    $hit['weight'] = ((float)($hit['weight'] ?? 1)) * 0.72;
+                    $hit['_token_matches'] = 0;
+                    $grouped[$id] = $hit;
+                }
+
+                $grouped[$id]['_token_matches']++;
+            }
+        }
+
+        $grouped = array_values($grouped);
+
+        usort($grouped, static function (array $a, array $b): int {
+            return ($b['_token_matches'] <=> $a['_token_matches'])
+                ?: (($b['weight'] ?? 0) <=> ($a['weight'] ?? 0));
+        });
+
+        return $grouped;
+    }
+    */
+
+    private function findTokenHits(string $queryTerm, array $terms): array
+    {
+        $tokens = (new QueryTokenizer())->analyze($queryTerm);
+
+        if ($tokens === []) {
+            return [];
+        }
+
+        $grouped = [];
+
+        foreach ($tokens as $tokenInfo) {
+            $token = $tokenInfo['token'];
+            $tokenWeight = (float)$tokenInfo['weight'];
+
+            foreach ($terms[$token] ?? [] as $hit) {
+                $id = (string)($hit['id'] ?? '');
+
+                if ($id === '') {
+                    continue;
+                }
+
+                $weightedHit = $hit;
+                $weightedHit['weight'] = ((float)($hit['weight'] ?? 1)) * $tokenWeight;
+                $weightedHit['_token_matches'] = 1;
+                $weightedHit['_token_origin'] = $tokenInfo['origin'];
+                $weightedHit['_matched_token'] = $token;
+
+                if (!isset($grouped[$id])) {
+                    $grouped[$id] = $weightedHit;
+                    continue;
+                }
+
+                $grouped[$id]['_token_matches']++;
+
+                if ($weightedHit['weight'] > ($grouped[$id]['weight'] ?? 0)) {
+                    $grouped[$id]['weight'] = $weightedHit['weight'];
+                    $grouped[$id]['_token_origin'] = $weightedHit['_token_origin'];
+                    $grouped[$id]['_matched_token'] = $weightedHit['_matched_token'];
+                }
+            }
+        }
+
+        $grouped = array_values($grouped);
+
+        usort($grouped, static function (array $a, array $b): int {
+            return ($b['_token_matches'] <=> $a['_token_matches'])
+                ?: (($b['weight'] ?? 0) <=> ($a['weight'] ?? 0));
+        });
+
+        return $grouped;
+    }
+
+
+    /** @param array<int|string, mixed> $properties */
+    private function qualifierFromContext(array $properties): ?string
+    {
+        foreach ($properties as $property) {
+            if (!is_array($property)) {
+                continue;
+            }
+
+            if (trim((string)($property['pid'] ?? '')) !== ContextTypeUri::QUALIFIER) {
+                continue;
+            }
+
+            $value = trim((string)($property['v'] ?? ''));
+            return $value !== '' ? $value : null;
+        }
+
+        return null;
+    }
+
+    public function preview(string $id, TenantContext $tenant): string
+    {
+        return '';
+    }
 }
\ No newline at end of file
--- /dev/null	2026-08-15 18:27:36.871710571 +0000
+++ src/Domain/Reconciliation/ContextTypeUri.php	2026-08-15 18:42:40.196999717 +0000
@@ -0,0 +1,13 @@
+<?php
+declare(strict_types=1);
+
+namespace App\Domain\Reconciliation;
+
+final class ContextTypeUri
+{
+    public const QUALIFIER = 'https://reconcilix.vocnet.org/context/0011';
+
+    private function __construct()
+    {
+    }
+}
--- /mnt/data/rx_pristine/reconcile_20260815_0/tests/Infrastructure/Discovery/Adapter/XtreeJsonApiAdapterTest.php	2026-08-01 15:18:45.000000000 +0000
+++ tests/Infrastructure/Discovery/Adapter/XtreeJsonApiAdapterTest.php	2026-08-15 18:42:14.163463661 +0000
@@ -16,9 +16,9 @@
     public array $args = [];
     public function __construct(private array $response) {}
     public function baseUrl(): string { return 'http://xtree.test'; }
-    public function searchVocItemsByTerm(string $vocabularyUri, string $searchTerm, string $language = 'all', int $jsonFull = 1): array
+    public function searchVocItemsByTerm(string $vocabularyUri, string $searchTerm, string $language = 'all', int $jsonFull = 1, ?string $qualifier = null): array
     { $this->method='search'; $this->args=func_get_args(); return $this->response; }
-    public function getFetchHierarchy(string $vocabularyUri, string $nodeId, string $searchTerm, string $language = 'all', int $jsonFull = 1): array
+    public function getFetchHierarchy(string $vocabularyUri, string $nodeId, string $searchTerm, string $language = 'all', int $jsonFull = 1, ?string $qualifier = null): array
     { $this->method='hierarchy'; $this->args=func_get_args(); return $this->response; }
 }
 
@@ -37,14 +37,16 @@
 $client=new FakeXtreeJsonApiClient($response);
 $adapter=new XtreeJsonApiAdapter($root,$scope,$client,new XtreeJsonApiResultMapper(),$tenant,$client->baseUrl());
 if (!$adapter instanceof CandidateDiscoveryAdapter) throw new RuntimeException('Contract missing.');
-$result=$adapter->discover(new ReconciliationCommand('q',' Schreiner ','CONCEPT',$root,$scope,3,'de'));
+$result=$adapter->discover(new ReconciliationCommand('q',' Schreiner ','CONCEPT',$root,$scope,3,'de',null,null,[['pid'=>'https://reconcilix.vocnet.org/context/0011','v'=>'Architektur']]));
 if ($client->method !== 'hierarchy' || ($client->args[1] ?? null) !== '00000375') throw new RuntimeException('Hierarchy call failed.');
+if (($client->args[5] ?? null) !== 'Architektur') throw new RuntimeException('Hierarchy qualifier forwarding failed.');
 if (count($result)!==1 || $result[0]->label!=='Stuhl' || $result[0]->score!==0.8) throw new RuntimeException('Mapping failed.');
 if ($result[0]->candidateProviderUri !== 'http://xtree.test') throw new RuntimeException('Provider URI failed.');
 
 $rootClient=new FakeXtreeJsonApiClient($response);
 $rootAdapter=new XtreeJsonApiAdapter($root,$root,$rootClient,new XtreeJsonApiResultMapper(),$tenant,$rootClient->baseUrl());
-$rootAdapter->discover(new ReconciliationCommand('q2','Altar','CONCEPT',$root,null,2));
+$rootAdapter->discover(new ReconciliationCommand('q2','Altar','CONCEPT',$root,null,2,null,null,null,[['pid'=>'https://reconcilix.vocnet.org/context/0011','v'=>'Ausstattung']]));
 if ($rootClient->method !== 'search') throw new RuntimeException('Root search failed.');
+if (($rootClient->args[4] ?? null) !== 'Ausstattung') throw new RuntimeException('Root qualifier forwarding failed.');
 
 echo "WP3-003 xTree JSON API Adapter: OK\n";
--- /mnt/data/rx_pristine/reconcile_20260815_0/tests/Infrastructure/Discovery/Adapter/LocalReconciliationStoreAdapterTest.php	2026-08-01 14:34:54.000000000 +0000
+++ tests/Infrastructure/Discovery/Adapter/LocalReconciliationStoreAdapterTest.php	2026-08-15 18:42:14.163606315 +0000
@@ -38,6 +38,15 @@
                 'qualifier' => 'Ausstattung',
                 'record_type' => 'concept',
             ],
+            [
+                'id' => 'urn:test:altar-architektur',
+                'label' => 'Altar',
+                'weight' => 1.0,
+                'role' => 'prefLabel',
+                'lang' => 'de',
+                'qualifier' => 'Architektur',
+                'record_type' => 'concept',
+            ],
         ],
     ], JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT));
 
@@ -94,6 +103,43 @@
         throw new RuntimeException('Candidate metadata mapping failed.');
     }
 
+    $qualified = $adapter->discover(new ReconciliationCommand(
+        requestId: 'q1-qualified',
+        sourceValue: 'Altar',
+        preferredEntityTypeUri: 'CONCEPT',
+        targetVocabularyUri: 'http://matcult-the.vocnet.org',
+        subVocabularyId: $scope,
+        limit: 3,
+        contextProperties: [[
+            'pid' => 'https://reconcilix.vocnet.org/context/0011',
+            'v' => 'Architektur',
+        ]],
+    ));
+
+    if (count($qualified) !== 1 || $qualified[0]->uri !== 'urn:test:altar-architektur') {
+        throw new RuntimeException('Qualifier-aware local-store lookup failed.');
+    }
+    if ($qualified[0]->label !== 'Altar (Architektur)') {
+        throw new RuntimeException('Qualifier-aware candidate label failed.');
+    }
+
+    $unknownQualifier = $adapter->discover(new ReconciliationCommand(
+        requestId: 'q1-qualified-empty',
+        sourceValue: 'Altar',
+        preferredEntityTypeUri: 'CONCEPT',
+        targetVocabularyUri: 'http://matcult-the.vocnet.org',
+        subVocabularyId: $scope,
+        limit: 3,
+        contextProperties: [[
+            'pid' => 'https://reconcilix.vocnet.org/context/0011',
+            'v' => 'Unbekannt',
+        ]],
+    ));
+
+    if ($unknownQualifier !== []) {
+        throw new RuntimeException('Unknown qualifier must return no local-store candidates.');
+    }
+
     $token = $adapter->discover(new ReconciliationCommand(
         requestId: 'q2',
         sourceValue: 'Hochaltäre',
--- /mnt/data/rx_pristine/reconcile_20260815_0/tests/Integration/XtreeDiscoveryAuditIntegrationTest.php	2026-08-08 06:04:28.000000000 +0000
+++ tests/Integration/XtreeDiscoveryAuditIntegrationTest.php	2026-08-15 18:42:29.185370490 +0000
@@ -21,7 +21,7 @@
 final class FailingXtreeClientForAudit extends XtreeJsonApiClient {
     public function __construct() {}
     public function baseUrl(): string {return 'http://xtree.test';}
-    public function searchVocItemsByTerm(string $vocabularyUri,string $searchTerm,string $language='all',int $jsonFull=1): array {
+    public function searchVocItemsByTerm(string $vocabularyUri,string $searchTerm,string $language='all',int $jsonFull=1,?string $qualifier=null): array {
         throw new ExternalServiceRequestException('xTree API returned invalid JSON.','xtree','getSearchVocItemsByTerm',DiscoveryAccessErrorType::INVALID_RESPONSE,200,21,'<html>broken</html>');
     }
 }
--- /mnt/data/rx_pristine/reconcile_20260815_0/tests/Integration/XtreeRuntimeIntegrationTest.php	2026-08-01 15:18:45.000000000 +0000
+++ tests/Integration/XtreeRuntimeIntegrationTest.php	2026-08-15 18:42:29.185720920 +0000
@@ -23,8 +23,8 @@
 {
     public function __construct() {}
     public function baseUrl(): string { return 'http://xtree.test'; }
-    public function searchVocItemsByTerm(string $vocabularyUri,string $searchTerm,string $language='all',int $jsonFull=1): array { return ['VocabularyItem'=>[]]; }
-    public function getFetchHierarchy(string $vocabularyUri,string $nodeId,string $searchTerm,string $language='all',int $jsonFull=1): array { return ['VocabularyItem'=>[]]; }
+    public function searchVocItemsByTerm(string $vocabularyUri,string $searchTerm,string $language='all',int $jsonFull=1,?string $qualifier=null): array { return ['VocabularyItem'=>[]]; }
+    public function getFetchHierarchy(string $vocabularyUri,string $nodeId,string $searchTerm,string $language='all',int $jsonFull=1,?string $qualifier=null): array { return ['VocabularyItem'=>[]]; }
 }
 final class RuntimeFakeSourceSystemFactory extends SourceSystemFactory
 {
