Change query param to query path

Change-Id: Ib01eb0b271924c11d3755029644ade293d520c47
diff --git a/README.md b/README.md
index a81510d..a1d476a 100644
--- a/README.md
+++ b/README.md
@@ -124,14 +124,65 @@
 
 ## API Endpoints
 
+### POST /query/:cfg
+
+Apply a cascade of query mappings to a JSON object. The `:cfg` path parameter specifies which mapping lists to apply and in what order, using a compact serialization format.
+
+**cfg format:** `id:dir[:foundryA:layerA:foundryB:layerB]` entries separated by `;`
+
+- `id`: ID of the mapping list
+- `dir`: Direction (`atob` or `btoa`)
+- Optional foundry/layer overrides (annotation mappings use 6 fields, corpus mappings use 4 fields with `fieldA:fieldB`)
+
+When override fields are omitted, defaults from the YAML mapping list are used.
+
+Request body: JSON object to transform
+
+Example request:
+
+```http
+POST /query/stts-upos:atob;other-mapper:btoa HTTP/1.1
+Content-Type: application/json
+
+{
+  "@type": "koral:token",
+  "wrap": {
+    "@type": "koral:term",
+    "foundry": "opennlp",
+    "key": "PIDAT",
+    "layer": "p",
+    "match": "match:eq"
+  }
+}
+```
+
+### POST /response/:cfg
+
+Apply a cascade of response mappings to a JSON object. The `:cfg` path parameter uses the same format as `/query/:cfg`.
+
+This endpoint processes response snippets by applying term mappings to annotations within HTML snippet markup, and enriches corpus fields for corpus mappings.
+
+Request body: JSON object (with `snippet` field for annotation mappings, or `fields` for corpus mappings)
+
+Example request:
+
+```http
+POST /response/stts-upos:btoa HTTP/1.1
+Content-Type: application/json
+
+{
+  "snippet": "<span title=\"marmot/m:gender:masc\">Der</span>"
+}
+```
+
 ### POST /:map/query
 
-Transform a JSON object using the specified mapping list.
+Transform a JSON object using a single mapping list.
 
 Parameters:
 
 - `:map`: ID of the mapping list to use
-- `dir` (query): Direction of transformation (atob or `btoa`, default: `atob`)
+- `dir` (query): Direction of transformation (`atob` or `btoa`, default: `atob`)
 - `foundryA` (query): Override default foundryA from mapping list
 - `foundryB` (query): Override default foundryB from mapping list
 - `layerA` (query): Override default layerA from mapping list
@@ -188,12 +239,12 @@
 
 ### POST /:map/response
 
-Transform JSON response objects using the specified mapping list. This endpoint processes response snippets by applying term mappings to annotations within HTML snippet markup.
+Transform JSON response objects using a single mapping list. This endpoint processes response snippets by applying term mappings to annotations within HTML snippet markup.
 
 Parameters:
 
 - `:map`: ID of the mapping list to use
-- `dir` (query): Direction of transformation (atob or `btoa`, default: `atob`)
+- `dir` (query): Direction of transformation (`atob` or `btoa`, default: `atob`)
 - `foundryA` (query): Override default foundryA from mapping list
 - `foundryB` (query): Override default foundryB from mapping list
 - `layerA` (query): Override default layerA from mapping list
@@ -220,9 +271,13 @@
 }
 ```
 
+### GET /
+
+Serves the configuration page for the Kalamar plugin integration. This HTML page allows selecting mapping lists and configuring their parameters. The JavaScript registers KorAP pipes using the path-based `/query/:cfg` and `/response/:cfg` endpoints.
+
 ### GET /:map
 
-Serves the Kalamar plugin integration page. This HTML page includes:
+Serves the Kalamar plugin integration page for a single mapping list. This HTML page includes:
 
 - Plugin information and available mapping lists
 - JavaScript integration code for Kalamar
@@ -230,6 +285,10 @@
 
 The SDK script and server data-attribute in the HTML are determined by the configuration file's `sdk` and `server` values, with fallback to default endpoints if not specified.
 
+### GET /health
+
+Health check endpoint. Returns `OK` with HTTP 200.
+
 ## Supported Mappings
 
 ### `mappings/stts-upos.yaml`
diff --git a/cmd/koralmapper/main.go b/cmd/koralmapper/main.go
index 732cc2b..d8e51a6 100644
--- a/cmd/koralmapper/main.go
+++ b/cmd/koralmapper/main.go
@@ -312,9 +312,9 @@
 	// Static file serving from embedded FS
 	app.Get("/static/*", handleStaticFile())
 
-	// Composite cascade transformation endpoints
-	app.Post("/query", handleCompositeQueryTransform(m, yamlConfig.Lists))
-	app.Post("/response", handleCompositeResponseTransform(m, yamlConfig.Lists))
+	// Composite cascade transformation endpoints (cfg in path)
+	app.Post("/query/:cfg", handleCompositeQueryTransform(m, yamlConfig.Lists))
+	app.Post("/response/:cfg", handleCompositeResponseTransform(m, yamlConfig.Lists))
 
 	// Transformation endpoint
 	app.Post("/:map/query", handleTransform(m))
@@ -406,7 +406,7 @@
 
 func handleCompositeQueryTransform(m *mapper.Mapper, lists []config.MappingList) fiber.Handler {
 	return func(c *fiber.Ctx) error {
-		cfgRaw := c.Query("cfg", "")
+		cfgRaw := c.Params("cfg")
 		if len(cfgRaw) > maxParamLength {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 				"error": fmt.Sprintf("cfg too long (max %d bytes)", maxParamLength),
@@ -465,7 +465,7 @@
 
 func handleCompositeResponseTransform(m *mapper.Mapper, lists []config.MappingList) fiber.Handler {
 	return func(c *fiber.Ctx) error {
-		cfgRaw := c.Query("cfg", "")
+		cfgRaw := c.Params("cfg")
 		if len(cfgRaw) > maxParamLength {
 			return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{
 				"error": fmt.Sprintf("cfg too long (max %d bytes)", maxParamLength),
diff --git a/cmd/koralmapper/main_test.go b/cmd/koralmapper/main_test.go
index 6121fe3..ecbb5e8 100644
--- a/cmd/koralmapper/main_test.go
+++ b/cmd/koralmapper/main_test.go
@@ -1595,7 +1595,7 @@
 	}{
 		{
 			name:         "cascades two query mappings",
-			url:          "/query?cfg=step1:atob;step2:atob",
+			url:          "/query/step1:atob;step2:atob",
 			expectedCode: http.StatusOK,
 			input: `{
 				"@type": "koral:token",
@@ -1619,33 +1619,8 @@
 			},
 		},
 		{
-			name:         "empty cfg returns input unchanged",
-			url:          "/query?cfg=",
-			expectedCode: http.StatusOK,
-			input: `{
-				"@type": "koral:token",
-				"wrap": {
-					"@type": "koral:term",
-					"foundry": "opennlp",
-					"key": "PIDAT",
-					"layer": "p",
-					"match": "match:eq"
-				}
-			}`,
-			expected: map[string]any{
-				"@type": "koral:token",
-				"wrap": map[string]any{
-					"@type":   "koral:term",
-					"foundry": "opennlp",
-					"key":     "PIDAT",
-					"layer":   "p",
-					"match":   "match:eq",
-				},
-			},
-		},
-		{
 			name:         "invalid cfg returns bad request",
-			url:          "/query?cfg=missing:atob",
+			url:          "/query/missing:atob",
 			expectedCode: http.StatusBadRequest,
 			input:        `{"@type": "koral:token"}`,
 			expected: map[string]any{
@@ -1703,7 +1678,7 @@
 	}{
 		{
 			name:         "cascades two response mappings",
-			url:          "/response?cfg=resp-step1:atob;resp-step2:atob",
+			url:          "/response/resp-step1:atob;resp-step2:atob",
 			expectedCode: http.StatusOK,
 			input: `{
 				"fields": [{
@@ -1724,27 +1699,8 @@
 			},
 		},
 		{
-			name:         "empty cfg returns input unchanged",
-			url:          "/response?cfg=",
-			expectedCode: http.StatusOK,
-			input: `{
-				"fields": [{
-					"@type": "koral:field",
-					"key": "textClass",
-					"value": "novel",
-					"type": "type:string"
-				}]
-			}`,
-			assertBody: func(t *testing.T, actual map[string]any) {
-				fields := actual["fields"].([]any)
-				require.Len(t, fields, 1)
-				assert.Equal(t, "textClass", fields[0].(map[string]any)["key"])
-				assert.Equal(t, "novel", fields[0].(map[string]any)["value"])
-			},
-		},
-		{
 			name:         "invalid cfg returns bad request",
-			url:          "/response?cfg=resp-step1",
+			url:          "/response/resp-step1",
 			expectedCode: http.StatusBadRequest,
 			input:        `{"fields": []}`,
 			assertBody: func(t *testing.T, actual map[string]any) {