Correcting serialization of "Der:sa /+w1 Betrieb" ("Der:sa" already
correct).
Bug 1 corrected: same ClassId for different spans.
Bug 2 corrected: object stack worked wrongly for prox and postion
conditions, see query above.
JUnits adapted, as classIds have modified values now.
1 JUnit added to check the correctness of the query above.
Changes updated.

Change-Id: Ife93b8992adc6d5c167a2644f12864b706d43ff1
Reviewed-on: https://korap.ids-mannheim.de/gerrit/c/KorAP/Koral/+/11138
Reviewed-by: <bodmer@ids-mannheim.de>
diff --git a/.gitignore b/.gitignore
index dc2f525..894c6d8 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,6 +11,8 @@
 .*
 !.gitignore
 !.github
+*.txt
+*.odt
 \#*\#
 
 # /src/main/antlr/cosmas/
diff --git a/Changes b/Changes
index a083c3b..eff1967 100644
--- a/Changes
+++ b/Changes
@@ -1,3 +1,10 @@
+0.52 2026-07-29
+     - [bugfix] C2QL: Correcting serialization of "Der:sa" combined with e.g. "/+w1 Betrieb".
+     - [bugfix] in case above: corrected: same ClassId for different spans.
+     - [bugfix] in case above: corrected: object stack worked wrongly for prox and position together
+     - [bugfix] JUnits adapted, as classIds have modified values now.
+     - [bugfix] JUnit added to check the correctness of the query above .
+
 0.51 2026-07-29
      - [feature] C2QL MORPH(v1 v2..): more than 1 value with/without foundry/layer specification works now.
      - [feature] translating MORPH expressions from C2-style STTS/CONNEXOR to STTS/marmot.
diff --git a/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java b/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java
index 5d2c744..a8aca8c 100644
--- a/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java
+++ b/src/main/java/de/ids_mannheim/korap/query/serialize/Cosmas2QueryProcessor.java
@@ -47,7 +47,10 @@
  * @author Nils Diewald (diewald@ids-mannheim.de)
  * @author Eliza Margaretha (margaretha@ids-mannheim.de)
  * @author Franck Bodmer (bodmer@ids-mannheim.de)
- * @version 0.4 -- 09.06.26/FB
+ * @version 0.4 -- 07.07.26/FB
+ *  - Korrektur: Doppelte Vergabe einer KlassenID.
+ *  - Korrektur: fehlerhafte Serialisierung von "Der:sa /+w1 Betrieb" (derweil "Der:sa" korrekt behandelt wurde)
+ *               infolge falscher Verwaltung des objectStacks.
  */
 public class Cosmas2QueryProcessor extends Antlr3AbstractQueryProcessor {
 
@@ -306,7 +309,10 @@
 
 
     private void processNode (Tree node) {
-        // Top-down processing
+    	
+    	final String func = "Debug: processNode";
+        
+    	// Top-down processing
         if (visited.contains(node))
             return;
         else
@@ -522,8 +528,10 @@
         // See C-II QL documentation for more detail:
         // http://www.ids-mannheim.de/cosmas2/win-app/hilfe/suchanfrage/eingabe-grafisch/syntax/textpositionen.html
 
+    	final String func = "Debug: processOPBED";
+    	
     	if( DEBUG )
-    		System.out.printf("Debug: processOPBED: '%s'.\n",  node.toStringTree());
+    		System.out.printf("%s: '%s'.\n", func, node.toStringTree());
     	
     	// Step I: create group
         int optsChild = node.getChildCount() - 1;
@@ -534,6 +542,7 @@
 
         Map<String, Object> submatchgroup =
                 KoralObjectGenerator.makeReference(classCounter + 128);
+        
         ArrayList<Object> submatchOperands = new ArrayList<Object>();
         submatchgroup.put("operands", submatchOperands);
         putIntoSuperObject(submatchgroup);
@@ -947,12 +956,12 @@
         if (!(openNodeCats.get(1).equals("OPBEG")
                 || openNodeCats.get(1).equals("OPEND") || inOPALL
                 || openNodeCats.get(1).equals("OPNHIT"))) {
-            wrapOperandInClass(node, 1, 128 + classCounter);
-            wrapOperandInClass(node, 2, 128 + classCounter);
+            wrapOperandInClass(node, 1, 128 + classCounter++);  // 07.07.26/FB
+            wrapOperandInClass(node, 2, 128 + classCounter++); // 07.07.26/FB
             // Deactivated, uncomment to wrap sequence in reference.
             //            group = KoralObjectGenerator.wrapInReference(group,
             //                    classCounter++);
-            classCounter++;
+            // classCounter++; 07.07.26/FB
         }
         else if (openNodeCats.get(1).equals("OPNHIT")) {
             Map<String, Object> repetition =
@@ -1850,6 +1859,8 @@
     
     private void processOPWF_OPLEM (Tree node) 
     {
+    	final String func = "Debug: processOPWF_OPLEM";
+    	
         String nodeCat = getNodeCat(node);
         // Step I: get info
         Map<String, Object> token = KoralObjectGenerator.makeToken();
@@ -1914,6 +1925,7 @@
             visited.add(node.getChild(0));
         }
         else {
+            
             // TODO
         	if( DEBUG )
         		System.out.printf("Error: processOPWF_OPLEM: TPOS not implemented: '%s'!\n", node.toStringTree());
@@ -2063,6 +2075,7 @@
         positionGroup.put("operands", posOperands);
         Map<String, Object> span = KoralObjectGenerator.makeSpan(elem);
         objectStack.push(classGroup);
+        stackedObjects++; // 07.07.26/FB
         if (hitSpanRef != null) {
             Map<String, Object> spanRefAroundHit = KoralObjectGenerator
                     .makeSpanReference(hitSpanRef, KoralOperation.FOCUS);
diff --git a/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java b/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java
index 1ee0aa2..0ac1341 100644
--- a/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java
+++ b/src/test/java/de/ids_mannheim/korap/test/cosmas2/Cosmas2QueryProcessorTest.java
@@ -1048,7 +1048,7 @@
         assertEquals("operation:class", res.at("/query/operands/0/operation")
                 .asText());
         assertEquals(129, res.at("/query/operands/0/classOut").asInt());
-        assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
         assertEquals("koral:token", res
                 .at("/query/operands/0/operands/0/@type").asText());
         assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
@@ -1076,7 +1076,7 @@
         assertEquals("operation:class", res.at("/query/operands/0/operation")
                 .asText());
         assertEquals(129, res.at("/query/operands/0/classOut").asInt());
-        assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/1/classOut").asInt());
         assertEquals("koral:token", res
                 .at("/query/operands/0/operands/0/@type").asText());
         assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
@@ -1105,7 +1105,7 @@
         assertEquals("operation:class", res.at("/query/operands/0/operation")
                 .asText());
         assertEquals(129, res.at("/query/operands/0/classOut").asInt());
-        assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/1/classOut").asInt());	// due to correction 07.07.26/FB
         assertEquals("koral:token", res
                 .at("/query/operands/0/operands/0/@type").asText());
         assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
@@ -1253,7 +1253,7 @@
         assertEquals(129, res.at("/query/operands/0/classOut").asInt());
         assertEquals("Sonne", res.at("/query/operands/0/operands/0/wrap/key")
                 .asText());
-        assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/1/classOut").asInt());
         assertEquals("operation:sequence",
                 res.at("/query/operands/1/operands/0/operation").asText());
         assertEquals("w", res
@@ -1264,14 +1264,14 @@
         assertEquals(7,
                 res.at("/query/operands/1/operands/0/distances/0/boundary/max")
                         .asInt());
-        assertEquals(130,
+        assertEquals(131,	// due to correction 07.07.26/FB
                 res.at("/query/operands/1/operands/0/operands/0/classOut")
                         .asInt());
         assertEquals(
                 "Mond",
                 res.at("/query/operands/1/operands/0/operands/0/operands/0/wrap/key")
                         .asText());
-        assertEquals(130,
+        assertEquals(132,	// due to correction 07.07.26/FB
                 res.at("/query/operands/1/operands/0/operands/1/classOut")
                         .asInt());
         assertEquals(
@@ -1310,7 +1310,7 @@
         assertEquals(129, res.at("/query/operands/1/classOut").asInt());
         assertEquals("Sonne", res.at("/query/operands/1/operands/0/wrap/key")
                 .asText());
-        assertEquals(129, res.at("/query/operands/0/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/0/classOut").asInt());	// due to correction 07.07.26/FB
         assertEquals("operation:sequence",
                 res.at("/query/operands/0/operands/0/operation").asText());
         assertEquals("w", res
@@ -1321,14 +1321,14 @@
         assertEquals(2,
                 res.at("/query/operands/0/operands/0/distances/0/boundary/max")
                         .asInt());
-        assertEquals(130,
+        assertEquals(131,	// due to correction 07.07.26/FB
                 res.at("/query/operands/0/operands/0/operands/0/classOut")
                         .asInt());
         assertEquals(
                 "Mond",
                 res.at("/query/operands/0/operands/0/operands/0/operands/0/wrap/key")
                         .asText());
-        assertEquals(130,
+        assertEquals(132,	// due to correction 07.07.26/FB
                 res.at("/query/operands/0/operands/0/operands/1/classOut")
                         .asInt());
         assertEquals(
@@ -1427,7 +1427,7 @@
                         .asText());
         assertEquals("operation:class", res.at("/query/operands/1/operation")
                 .asText());
-        assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/1/classOut").asInt());	// due to correction 07.07.26/FB
         assertEquals("koral:token", res
                 .at("/query/operands/1/operands/0/@type").asText());
         assertEquals("kommt", res.at("/query/operands/1/operands/0/wrap/key")
@@ -1447,7 +1447,7 @@
         assertEquals("koral:group", res.at("/query/operands/1/@type").asText());
         assertEquals("operation:class", res.at("/query/operands/1/operation")
                 .asText());
-        assertEquals(129, res.at("/query/operands/1/classOut").asInt());
+        assertEquals(130, res.at("/query/operands/1/classOut").asInt());	// due to correction 07.07.26/FB
         assertEquals("koral:reference",
                 res.at("/query/operands/1/operands/0/@type").asText());
         assertEquals("operation:focus",
@@ -2160,7 +2160,7 @@
                 res.at("/query/operands/0/operands/0/operands/1/operands/0/operands/0/operands/0/wrap/key")
                         .asText());
         assertEquals(
-                132,
+                133,	// due to correction 07.07.26/FB
                 res.at("/query/operands/0/operands/0/operands/1/operands/0/operands/1/classOut")
                         .asInt());
         assertEquals(
@@ -2455,7 +2455,13 @@
                 "Mann",
                 res.at("/query/operands/0/operands/1/operands/1/operands/0/operands/1/wrap/key")
                         .asText());
-
+		
+        // check the combination of Positional condition and proximity operator,
+        // this was corrected by 07.07.26/FB
+        
+		query = "Der:sa /+w1 Betrieb";
+		qs.setQuery(query, "cosmas2");
+		res = mapper.readTree(qs.toJSON());
     }
 
 
@@ -2718,6 +2724,103 @@
 */
     }
 
+
+    /**
+     * Regression test for a bug where a sequence combining a
+     * position operator (OPBED, e.g. "sa" = starts sentence) with
+     * a distance operator (OPPROX, e.g. "/+w1") produced an invalid
+     * KoralQuery: the second sequence operand ended up nested
+     * *inside* the koral:group/operation:class wrapping the first
+     * operand, instead of being its sibling in the sequence's
+     * operands array. This violated the requirement that
+     * operation:class expects exactly one operand, and caused
+     * KorAP/Krill to reject the query.
+     * 
+     * Root cause: processPositionCondition() pushed a class group
+     * onto the internal objectStack (to collect the wrapped token)
+     * but never recorded this push via stackedObjects++, so the
+     * generic per-node stack bookkeeping in processNode() failed to
+     * pop it again. This left one stale object on the stack, which
+     * shifted where the second sequence operand got inserted.
+     */
+    @Test
+    public void testOPBEDinOPPROXSequence () throws JsonProcessingException,
+            IOException {
+        query = "Der:sa /+w1 Betrieb";
+        qs.setQuery(query, "cosmas2");
+        res = mapper.readTree(qs.toJSON());
+
+        // Top level: a 2-operand sequence with the distance constraint
+        assertEquals("koral:group", res.at("/query/@type").asText());
+        assertEquals("operation:sequence", res.at("/query/operation")
+                .asText());
+        assertTrue(res.at("/query/inOrder").asBoolean());
+        assertEquals("cosmas:distance", res.at("/query/distances/0/@type")
+                .asText());
+        assertEquals("w", res.at("/query/distances/0/key").asText());
+        assertEquals(0, res.at("/query/distances/0/boundary/min").asInt());
+        assertEquals(1, res.at("/query/distances/0/boundary/max").asInt());
+        assertEquals(2, res.at("/query/operands").size());
+
+        // First operand: "Der" at sentence start, wrapped in its own class
+        assertEquals("koral:group", res.at("/query/operands/0/@type")
+                .asText());
+        assertEquals("operation:class",
+                res.at("/query/operands/0/operation").asText());
+        int firstClass = res.at("/query/operands/0/classOut").asInt();
+
+        assertEquals("koral:reference",
+                res.at("/query/operands/0/operands/0/@type").asText());
+        assertEquals("operation:focus",
+                res.at("/query/operands/0/operands/0/operation").asText());
+        int innerClass = res
+                .at("/query/operands/0/operands/0/classRef/0").asInt();
+
+        assertEquals(
+                "koral:group",
+                res.at("/query/operands/0/operands/0/operands/0/@type")
+                        .asText());
+        assertEquals(
+                "operation:position",
+                res.at("/query/operands/0/operands/0/operands/0/operation")
+                        .asText());
+        assertEquals(
+                "frames:startsWith",
+                res.at("/query/operands/0/operands/0/operands/0/frames/0")
+                        .asText());
+        assertEquals(
+                "s",
+                res.at("/query/operands/0/operands/0/operands/0/operands/0/wrap/key")
+                        .asText());
+        assertEquals(
+                innerClass,
+                res.at("/query/operands/0/operands/0/operands/0/operands/1/classOut")
+                        .asInt());
+        assertEquals(
+                "Der",
+                res.at("/query/operands/0/operands/0/operands/0/operands/1/operands/0/wrap/key")
+                        .asText());
+
+        // Second operand: "Betrieb" - a direct sibling of the first
+        // operand in the sequence, NOT nested inside it.
+        assertEquals("koral:group", res.at("/query/operands/1/@type")
+                .asText());
+        assertEquals("operation:class",
+                res.at("/query/operands/1/operation").asText());
+        assertEquals("koral:token",
+                res.at("/query/operands/1/operands/0/@type").asText());
+        assertEquals("Betrieb",
+                res.at("/query/operands/1/operands/0/wrap/key").asText());
+
+        // The sequence itself must not have picked up a spurious
+        // third, deeply-nested operand.
+        assertTrue(res.at("/query/operands/0/operands/1").isMissingNode());
+
+        // Class numbers used within a query must be distinct.
+        assertNotEquals(firstClass, innerClass);
+    }
+
+
     @Test
     public void testWildcard () throws JsonProcessingException, IOException {
         query = "meine* /+w1:2,s0 &Erfahrung";
diff --git a/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java b/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java
index b7a1c5d..636b91d 100644
--- a/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java
+++ b/src/test/java/de/ids_mannheim/korap/test/cosmas2/OPOVTest.java
@@ -17,6 +17,8 @@
 import de.ids_mannheim.korap.query.object.KoralOperation;
 import de.ids_mannheim.korap.query.serialize.QuerySerializer;
 
+// Anpassung der inkorrekten JUnits nach der Korrektur vom 07.07.26/FB an Cosmas2QueryProcessor.java.
+
 public class OPOVTest {
 
     String query;
@@ -237,6 +239,6 @@
                 res.at("/operation").asText());
         // class
         assertEquals(130, res.at("/operands/0/classOut").asInt());
-        assertEquals(130, res.at("/operands/1/classOut").asInt());
+        assertEquals(131, res.at("/operands/1/classOut").asInt()); // due to correction 07.07.26/FB
     }
 }