Fix foundry and layer handling in attribute groups

Change-Id: I638d96ea03f75349312c95dc0a08bee457f048db
diff --git a/Changes b/Changes
index d78dca0..5a580b1 100644
--- a/Changes
+++ b/Changes
@@ -1,6 +1,8 @@
 0.64.7 2026-04-28
     - [bugfix] Keep highlights that extend beyond a cut match
       (diewald; fixes #177; diewald; AI-assisted Claude Opus 4.6)
+    - [bugfix] Correctly handle foundry and layer in attribute groups
+      (diewald; AI-assisted Claude Opus 4.6)
 
 0.64.6 2026-03-09
     - [performance] Add leaf cache. (diewald)
diff --git a/src/main/java/de/ids_mannheim/korap/KrillQuery.java b/src/main/java/de/ids_mannheim/korap/KrillQuery.java
index 82d3086..c71eb1a 100644
--- a/src/main/java/de/ids_mannheim/korap/KrillQuery.java
+++ b/src/main/java/de/ids_mannheim/korap/KrillQuery.java
@@ -1551,6 +1551,18 @@
         String relation = attrNode.get("relation").asText();
         JsonNode operands = attrNode.get("operands");
 
+        // Copy foundry and layer from the attribute group to the operands
+        if (attrNode.has("foundry") || attrNode.has("layer")) {
+            for (JsonNode operand : operands) {
+                if (attrNode.has("foundry") && !operand.has("foundry")) {
+                    ((ObjectNode) operand).set("foundry", attrNode.get("foundry"));
+                }
+                if (attrNode.has("layer") && !operand.has("layer")) {
+                    ((ObjectNode) operand).set("layer", attrNode.get("layer"));
+                }
+            }
+        }
+
         SpanQueryWrapper attrWrapper;
         if ("relation:and".equals(relation)) {
             List<SpanQueryWrapper> wrapperList = new ArrayList<SpanQueryWrapper>();
diff --git a/src/test/java/de/ids_mannheim/korap/index/TestAttributeIndex.java b/src/test/java/de/ids_mannheim/korap/index/TestAttributeIndex.java
index 06fcf18..2ea1795 100644
--- a/src/test/java/de/ids_mannheim/korap/index/TestAttributeIndex.java
+++ b/src/test/java/de/ids_mannheim/korap/index/TestAttributeIndex.java
@@ -547,4 +547,51 @@
         Result kr = krill.apply(ki);
         assertEquals(4, kr.getTotalResults());
     }
+
+    @Test
+    public void testAttributeRealIndexAndGroup () throws QueryException, IOException {
+        KrillIndex ki = new KrillIndex();
+        ki.addDoc(getClass().getResourceAsStream("/others/REDEW-DOC1-00001.json.gz"),
+                    true);
+        ki.commit();
+
+        String filepath = getClass()
+                .getResource(
+                        "/queries/attribute/said-and-attributes.jsonld")
+                .getFile();
+
+        SpanQueryWrapper sqw = getJsonQuery(filepath);
+        Krill krill = new Krill(sqw);
+        assertEquals(
+                "spanElementWithAttribute(<tokens:dereko/s:said />, "
+                        + "[spanAttribute(tokens:@:dereko/s:mode:direct), "
+                        + "spanAttribute(tokens:@:dereko/s:content:speech)])",
+                krill.getSpanQuery().toString());
+        Result kr = krill.apply(ki);
+        assertEquals(2, kr.getTotalResults());
+    }
+
+    @Test
+    public void testAttributeRealIndexOrGroup () throws QueryException, IOException {
+        KrillIndex ki = new KrillIndex();
+        ki.addDoc(getClass().getResourceAsStream("/others/REDEW-DOC1-00001.json.gz"),
+                    true);
+        ki.commit();
+
+        String filepath = getClass()
+                .getResource(
+                        "/queries/attribute/said-or-attributes.jsonld")
+                .getFile();
+
+        SpanQueryWrapper sqw = getJsonQuery(filepath);
+        Krill krill = new Krill(sqw);
+        assertEquals(
+                "spanOr([spanElementWithAttribute(<tokens:dereko/s:said />, "
+                        + "spanAttribute(tokens:@:dereko/s:mode:direct)), "
+                        + "spanElementWithAttribute(<tokens:dereko/s:said />, "
+                        + "spanAttribute(tokens:@:dereko/s:mode:indirect))])",
+                krill.getSpanQuery().toString());
+        Result kr = krill.apply(ki);
+        assertEquals(6, kr.getTotalResults());
+    }
 }
diff --git a/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java b/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java
index 89e110b..877a37f 100644
--- a/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java
+++ b/src/test/java/de/ids_mannheim/korap/query/TestSpanWithAttributeJSON.java
@@ -189,6 +189,37 @@
 
 
     @Test
+    public void testElementMultipleAndAttributesWithFoundryAndLayer () throws QueryException {
+        String filepath = getClass()
+                .getResource(
+                        "/queries/attribute/element-multiple-and-attributes-with-fl.jsonld")
+                .getFile();
+        SpanQueryWrapper sqwi = getJsonQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "spanElementWithAttribute(<tokens:dereko/s:head />, "
+                        + "[spanAttribute(tokens:@:dereko/s:type:top), "
+                        + "spanAttribute(tokens:@:dereko/s:type:main)])",
+                sq.toString());
+    }
+
+    @Test
+    public void testElementMultipleOrAttributesWithFoundryAndLayer () throws QueryException {
+        String filepath = getClass()
+                .getResource(
+                        "/queries/attribute/element-multiple-or-attributes-with-fl.jsonld")
+                .getFile();
+        SpanQueryWrapper sqwi = getJsonQuery(filepath);
+        SpanQuery sq = sqwi.toQuery();
+        assertEquals(
+                "spanOr([spanElementWithAttribute(<tokens:dereko/s:head />, "
+                        + "spanAttribute(tokens:@:dereko/s:type:top)), "
+                        + "spanElementWithAttribute(<tokens:dereko/s:head />, "
+                        + "spanAttribute(tokens:@:dereko/s:type:main))])",
+                sq.toString());
+    }
+
+    @Test
     public void testAnyElementSingleNotAttribute () throws QueryException {
 
         String filepath = getClass()
diff --git a/src/test/resources/queries/attribute/element-multiple-and-attributes-with-fl.jsonld b/src/test/resources/queries/attribute/element-multiple-and-attributes-with-fl.jsonld
new file mode 100644
index 0000000..83e2f57
--- /dev/null
+++ b/src/test/resources/queries/attribute/element-multiple-and-attributes-with-fl.jsonld
@@ -0,0 +1,37 @@
+{
+  "@context": "http://ids-mannheim.de/ns/KorAP/json-ld/v0.2/context.jsonld",
+  "errors": [],
+  "warnings": [],
+  "messages": [],
+  "collection": {},
+  "query": {
+    "@type": "koral:span",
+    "attr": {
+      "@type": "koral:termGroup",
+      "foundry": "dereko",
+      "layer": "s",
+      "operands": [
+        {
+          "@type": "koral:term",
+          "key": "type",
+          "match": "match:eq",
+          "value": "top"
+        },
+        {
+          "@type": "koral:term",
+          "key": "type",
+          "match": "match:eq",
+          "value": "main"
+        }
+      ],
+      "relation": "relation:and"
+    },
+    "wrap": {
+      "@type": "koral:term",
+      "foundry": "dereko",
+      "key": "head",
+      "layer": "s"
+    }
+  },
+  "meta": {}
+}
diff --git a/src/test/resources/queries/attribute/element-multiple-or-attributes-with-fl.jsonld b/src/test/resources/queries/attribute/element-multiple-or-attributes-with-fl.jsonld
new file mode 100644
index 0000000..1126df2
--- /dev/null
+++ b/src/test/resources/queries/attribute/element-multiple-or-attributes-with-fl.jsonld
@@ -0,0 +1,37 @@
+{
+  "@context": "http://ids-mannheim.de/ns/KorAP/json-ld/v0.2/context.jsonld",
+  "errors": [],
+  "warnings": [],
+  "messages": [],
+  "collection": {},
+  "query": {
+    "@type": "koral:span",
+    "attr": {
+      "@type": "koral:termGroup",
+      "foundry": "dereko",
+      "layer": "s",
+      "operands": [
+        {
+          "@type": "koral:term",
+          "key": "type",
+          "match": "match:eq",
+          "value": "top"
+        },
+        {
+          "@type": "koral:term",
+          "key": "type",
+          "match": "match:eq",
+          "value": "main"
+        }
+      ],
+      "relation": "relation:or"
+    },
+    "wrap": {
+      "@type": "koral:term",
+      "foundry": "dereko",
+      "key": "head",
+      "layer": "s"
+    }
+  },
+  "meta": {}
+}
diff --git a/src/test/resources/queries/attribute/said-and-attributes.jsonld b/src/test/resources/queries/attribute/said-and-attributes.jsonld
new file mode 100644
index 0000000..cb4a74d
--- /dev/null
+++ b/src/test/resources/queries/attribute/said-and-attributes.jsonld
@@ -0,0 +1,37 @@
+{
+  "@context": "http://ids-mannheim.de/ns/KorAP/json-ld/v0.2/context.jsonld",
+  "errors": [],
+  "warnings": [],
+  "messages": [],
+  "collection": {},
+  "query": {
+    "@type": "koral:span",
+    "attr": {
+      "@type": "koral:termGroup",
+      "foundry": "dereko",
+      "layer": "s",
+      "operands": [
+        {
+          "@type": "koral:term",
+          "key": "mode",
+          "match": "match:eq",
+          "value": "direct"
+        },
+        {
+          "@type": "koral:term",
+          "key": "content",
+          "match": "match:eq",
+          "value": "speech"
+        }
+      ],
+      "relation": "relation:and"
+    },
+    "wrap": {
+      "@type": "koral:term",
+      "foundry": "dereko",
+      "key": "said",
+      "layer": "s"
+    }
+  },
+  "meta": {}
+}
diff --git a/src/test/resources/queries/attribute/said-or-attributes.jsonld b/src/test/resources/queries/attribute/said-or-attributes.jsonld
new file mode 100644
index 0000000..3b027f0
--- /dev/null
+++ b/src/test/resources/queries/attribute/said-or-attributes.jsonld
@@ -0,0 +1,37 @@
+{
+  "@context": "http://ids-mannheim.de/ns/KorAP/json-ld/v0.2/context.jsonld",
+  "errors": [],
+  "warnings": [],
+  "messages": [],
+  "collection": {},
+  "query": {
+    "@type": "koral:span",
+    "attr": {
+      "@type": "koral:termGroup",
+      "foundry": "dereko",
+      "layer": "s",
+      "operands": [
+        {
+          "@type": "koral:term",
+          "key": "mode",
+          "match": "match:eq",
+          "value": "direct"
+        },
+        {
+          "@type": "koral:term",
+          "key": "mode",
+          "match": "match:eq",
+          "value": "indirect"
+        }
+      ],
+      "relation": "relation:or"
+    },
+    "wrap": {
+      "@type": "koral:term",
+      "foundry": "dereko",
+      "key": "said",
+      "layer": "s"
+    }
+  },
+  "meta": {}
+}