Remove pkg subfolder
diff --git a/matcher/matcher.go b/matcher/matcher.go
new file mode 100644
index 0000000..7542bdf
--- /dev/null
+++ b/matcher/matcher.go
@@ -0,0 +1,443 @@
+package matcher
+
+import (
+	"fmt"
+
+	"github.com/KorAP/KoralPipe-TermMapper2/ast"
+)
+
+// Matcher handles pattern matching and replacement in the AST
+type Matcher struct {
+	pattern     ast.Pattern
+	replacement ast.Replacement
+}
+
+// validateNode checks if a node is valid for pattern/replacement ASTs
+func validateNode(node ast.Node) error {
+	if node == nil {
+		return fmt.Errorf("nil node")
+	}
+
+	switch n := node.(type) {
+	case *ast.Token:
+		if n.Wrap != nil {
+			return validateNode(n.Wrap)
+		}
+		return nil
+	case *ast.Term:
+		return nil
+	case *ast.TermGroup:
+		if len(n.Operands) == 0 {
+			return fmt.Errorf("empty term group")
+		}
+		for _, op := range n.Operands {
+			if err := validateNode(op); err != nil {
+				return fmt.Errorf("invalid operand: %v", err)
+			}
+		}
+		return nil
+	case *ast.CatchallNode:
+		return fmt.Errorf("catchall nodes are not allowed in pattern/replacement ASTs")
+	default:
+		return fmt.Errorf("unknown node type: %T", node)
+	}
+}
+
+// NewMatcher creates a new Matcher with the given pattern and replacement
+func NewMatcher(pattern ast.Pattern, replacement ast.Replacement) (*Matcher, error) {
+	if err := validateNode(pattern.Root); err != nil {
+		return nil, fmt.Errorf("invalid pattern: %v", err)
+	}
+	if err := validateNode(replacement.Root); err != nil {
+		return nil, fmt.Errorf("invalid replacement: %v", err)
+	}
+	return &Matcher{
+		pattern:     pattern,
+		replacement: replacement,
+	}, nil
+}
+
+// Match checks if the given node matches the pattern
+func (m *Matcher) Match(node ast.Node) bool {
+	return m.matchNode(node, m.pattern.Root)
+}
+
+// Replace replaces all occurrences of the pattern in the given node with the replacement
+func (m *Matcher) Replace(node ast.Node) ast.Node {
+	// First step: Create complete structure with replacements
+	replaced := m.replaceNode(node)
+	// Second step: Simplify the structure
+	simplified := m.simplifyNode(replaced)
+	// If the input was a Token, ensure the output is also a Token
+	if _, isToken := node.(*ast.Token); isToken {
+		if _, isToken := simplified.(*ast.Token); !isToken {
+			return &ast.Token{Wrap: simplified}
+		}
+	}
+	return simplified
+}
+
+// replaceNode creates a complete structure with replacements
+func (m *Matcher) replaceNode(node ast.Node) ast.Node {
+	if node == nil {
+		return nil
+	}
+
+	// First handle Token nodes specially to preserve their structure
+	if token, ok := node.(*ast.Token); ok {
+		if token.Wrap == nil {
+			return token
+		}
+		// Process the wrapped node
+		wrap := m.replaceNode(token.Wrap)
+		return &ast.Token{Wrap: wrap}
+	}
+
+	// If this node matches the pattern
+	if m.Match(node) {
+		// For TermGroups that contain a matching Term, preserve unmatched operands
+		if tg, ok := node.(*ast.TermGroup); ok {
+			// Check if any operand matches the pattern exactly
+			hasExactMatch := false
+			for _, op := range tg.Operands {
+				if m.matchNode(op, m.pattern.Root) {
+					hasExactMatch = true
+					break
+				}
+			}
+
+			// If we have an exact match, replace matching operands
+			if hasExactMatch {
+				hasMatch := false
+				newOperands := make([]ast.Node, 0, len(tg.Operands))
+				for _, op := range tg.Operands {
+					if m.matchNode(op, m.pattern.Root) {
+						if !hasMatch {
+							newOperands = append(newOperands, m.cloneNode(m.replacement.Root))
+							hasMatch = true
+						} else {
+							newOperands = append(newOperands, m.replaceNode(op))
+						}
+					} else {
+						newOperands = append(newOperands, m.replaceNode(op))
+					}
+				}
+				return &ast.TermGroup{
+					Operands: newOperands,
+					Relation: tg.Relation,
+				}
+			}
+			// Otherwise, replace the entire TermGroup
+			return m.cloneNode(m.replacement.Root)
+		}
+		// For other nodes, return the replacement
+		return m.cloneNode(m.replacement.Root)
+	}
+
+	// Otherwise recursively process children
+	switch n := node.(type) {
+	case *ast.TermGroup:
+		// Check if any operand matches the pattern exactly
+		hasExactMatch := false
+		for _, op := range n.Operands {
+			if m.matchNode(op, m.pattern.Root) {
+				hasExactMatch = true
+				break
+			}
+		}
+
+		// If we have an exact match, replace matching operands
+		if hasExactMatch {
+			hasMatch := false
+			newOperands := make([]ast.Node, 0, len(n.Operands))
+			for _, op := range n.Operands {
+				if m.matchNode(op, m.pattern.Root) {
+					if !hasMatch {
+						newOperands = append(newOperands, m.cloneNode(m.replacement.Root))
+						hasMatch = true
+					} else {
+						newOperands = append(newOperands, m.replaceNode(op))
+					}
+				} else {
+					newOperands = append(newOperands, m.replaceNode(op))
+				}
+			}
+			return &ast.TermGroup{
+				Operands: newOperands,
+				Relation: n.Relation,
+			}
+		}
+		// Otherwise, recursively process operands
+		newOperands := make([]ast.Node, len(n.Operands))
+		for i, op := range n.Operands {
+			newOperands[i] = m.replaceNode(op)
+		}
+		return &ast.TermGroup{
+			Operands: newOperands,
+			Relation: n.Relation,
+		}
+
+	case *ast.CatchallNode:
+		newNode := &ast.CatchallNode{
+			NodeType:   n.NodeType,
+			RawContent: n.RawContent,
+		}
+		if n.Wrap != nil {
+			newNode.Wrap = m.replaceNode(n.Wrap)
+		}
+		if len(n.Operands) > 0 {
+			newNode.Operands = make([]ast.Node, len(n.Operands))
+			for i, op := range n.Operands {
+				newNode.Operands[i] = m.replaceNode(op)
+			}
+		}
+		return newNode
+
+	default:
+		return node
+	}
+}
+
+// simplifyNode removes unnecessary wrappers and empty nodes
+func (m *Matcher) simplifyNode(node ast.Node) ast.Node {
+	if node == nil {
+		return nil
+	}
+
+	switch n := node.(type) {
+	case *ast.Token:
+		if n.Wrap == nil {
+			return nil
+		}
+		simplified := m.simplifyNode(n.Wrap)
+		if simplified == nil {
+			return nil
+		}
+		return &ast.Token{Wrap: simplified}
+
+	case *ast.TermGroup:
+		// First simplify all operands
+		simplified := make([]ast.Node, 0, len(n.Operands))
+		for _, op := range n.Operands {
+			if s := m.simplifyNode(op); s != nil {
+				simplified = append(simplified, s)
+			}
+		}
+
+		// Handle special cases
+		if len(simplified) == 0 {
+			return nil
+		}
+		if len(simplified) == 1 {
+			// If we have a single operand, return it directly
+			// But only if we're not inside a Token
+			if _, isToken := node.(*ast.Token); !isToken {
+				return simplified[0]
+			}
+		}
+
+		return &ast.TermGroup{
+			Operands: simplified,
+			Relation: n.Relation,
+		}
+
+	case *ast.CatchallNode:
+		newNode := &ast.CatchallNode{
+			NodeType:   n.NodeType,
+			RawContent: n.RawContent,
+		}
+		if n.Wrap != nil {
+			newNode.Wrap = m.simplifyNode(n.Wrap)
+		}
+		if len(n.Operands) > 0 {
+			simplified := make([]ast.Node, 0, len(n.Operands))
+			for _, op := range n.Operands {
+				if s := m.simplifyNode(op); s != nil {
+					simplified = append(simplified, s)
+				}
+			}
+			if len(simplified) > 0 {
+				newNode.Operands = simplified
+			}
+		}
+		return newNode
+
+	default:
+		return node
+	}
+}
+
+// matchNode recursively checks if two nodes match
+func (m *Matcher) matchNode(node, pattern ast.Node) bool {
+	if pattern == nil {
+		return true
+	}
+	if node == nil {
+		return false
+	}
+
+	// Handle pattern being a Token
+	if pToken, ok := pattern.(*ast.Token); ok {
+		if nToken, ok := node.(*ast.Token); ok {
+			return m.matchNode(nToken.Wrap, pToken.Wrap)
+		}
+		return false
+	}
+
+	// Handle pattern being a Term
+	if pTerm, ok := pattern.(*ast.Term); ok {
+		// Direct term to term matching
+		if t, ok := node.(*ast.Term); ok {
+			return t.Foundry == pTerm.Foundry &&
+				t.Key == pTerm.Key &&
+				t.Layer == pTerm.Layer &&
+				t.Match == pTerm.Match &&
+				(pTerm.Value == "" || t.Value == pTerm.Value)
+		}
+		// If node is a Token, check its wrap
+		if tkn, ok := node.(*ast.Token); ok {
+			if tkn.Wrap == nil {
+				return false
+			}
+			return m.matchNode(tkn.Wrap, pattern)
+		}
+		// If node is a TermGroup, check its operands
+		if tg, ok := node.(*ast.TermGroup); ok {
+			for _, op := range tg.Operands {
+				if m.matchNode(op, pattern) {
+					return true
+				}
+			}
+			return false
+		}
+		// If node is a CatchallNode, check its wrap and operands
+		if c, ok := node.(*ast.CatchallNode); ok {
+			if c.Wrap != nil && m.matchNode(c.Wrap, pattern) {
+				return true
+			}
+			for _, op := range c.Operands {
+				if m.matchNode(op, pattern) {
+					return true
+				}
+			}
+			return false
+		}
+		return false
+	}
+
+	// Handle pattern being a TermGroup
+	if pGroup, ok := pattern.(*ast.TermGroup); ok {
+		// For OR relations, check if any operand matches the node
+		if pGroup.Relation == ast.OrRelation {
+			for _, pOp := range pGroup.Operands {
+				if m.matchNode(node, pOp) {
+					return true
+				}
+			}
+			return false
+		}
+
+		// For AND relations, node must be a TermGroup with matching relation
+		if tg, ok := node.(*ast.TermGroup); ok {
+			if tg.Relation != pGroup.Relation {
+				return false
+			}
+			// Check that all pattern operands match in any order
+			if len(tg.Operands) < len(pGroup.Operands) {
+				return false
+			}
+			matched := make([]bool, len(tg.Operands))
+			for _, pOp := range pGroup.Operands {
+				found := false
+				for j, tOp := range tg.Operands {
+					if !matched[j] && m.matchNode(tOp, pOp) {
+						matched[j] = true
+						found = true
+						break
+					}
+				}
+				if !found {
+					return false
+				}
+			}
+			return true
+		}
+
+		// If node is a Token, check its wrap
+		if tkn, ok := node.(*ast.Token); ok {
+			if tkn.Wrap == nil {
+				return false
+			}
+			return m.matchNode(tkn.Wrap, pattern)
+		}
+
+		// If node is a CatchallNode, check its wrap and operands
+		if c, ok := node.(*ast.CatchallNode); ok {
+			if c.Wrap != nil && m.matchNode(c.Wrap, pattern) {
+				return true
+			}
+			for _, op := range c.Operands {
+				if m.matchNode(op, pattern) {
+					return true
+				}
+			}
+			return false
+		}
+
+		return false
+	}
+
+	return false
+}
+
+// cloneNode creates a deep copy of a node
+func (m *Matcher) cloneNode(node ast.Node) ast.Node {
+	if node == nil {
+		return nil
+	}
+
+	switch n := node.(type) {
+	case *ast.Token:
+		return &ast.Token{
+			Wrap: m.cloneNode(n.Wrap),
+		}
+
+	case *ast.TermGroup:
+		operands := make([]ast.Node, len(n.Operands))
+		for i, op := range n.Operands {
+			operands[i] = m.cloneNode(op)
+		}
+		return &ast.TermGroup{
+			Operands: operands,
+			Relation: n.Relation,
+		}
+
+	case *ast.Term:
+		return &ast.Term{
+			Foundry: n.Foundry,
+			Key:     n.Key,
+			Layer:   n.Layer,
+			Match:   n.Match,
+			Value:   n.Value,
+		}
+
+	case *ast.CatchallNode:
+		newNode := &ast.CatchallNode{
+			NodeType:   n.NodeType,
+			RawContent: n.RawContent,
+		}
+		if n.Wrap != nil {
+			newNode.Wrap = m.cloneNode(n.Wrap)
+		}
+		if len(n.Operands) > 0 {
+			newNode.Operands = make([]ast.Node, len(n.Operands))
+			for i, op := range n.Operands {
+				newNode.Operands[i] = m.cloneNode(op)
+			}
+		}
+		return newNode
+
+	default:
+		return nil
+	}
+}
diff --git a/matcher/matcher_test.go b/matcher/matcher_test.go
new file mode 100644
index 0000000..3f05edc
--- /dev/null
+++ b/matcher/matcher_test.go
@@ -0,0 +1,812 @@
+package matcher
+
+// matcher is a function that takes a pattern and a node and returns true if the node matches the pattern.
+// It is used to match a pattern against a node in the AST.
+
+import (
+	"encoding/json"
+	"testing"
+
+	"github.com/KorAP/KoralPipe-TermMapper2/ast"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestNewMatcherValidation(t *testing.T) {
+	tests := []struct {
+		name          string
+		pattern       ast.Pattern
+		replacement   ast.Replacement
+		expectedError string
+	}{
+		{
+			name: "Valid pattern and replacement",
+			pattern: ast.Pattern{
+				Root: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			replacement: ast.Replacement{
+				Root: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "COMBINED_DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expectedError: "",
+		},
+		{
+			name: "Invalid pattern - CatchallNode",
+			pattern: ast.Pattern{
+				Root: &ast.CatchallNode{
+					NodeType: "custom",
+				},
+			},
+			replacement: ast.Replacement{
+				Root: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expectedError: "invalid pattern: catchall nodes are not allowed in pattern/replacement ASTs",
+		},
+		{
+			name: "Invalid replacement - CatchallNode",
+			pattern: ast.Pattern{
+				Root: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			replacement: ast.Replacement{
+				Root: &ast.CatchallNode{
+					NodeType: "custom",
+				},
+			},
+			expectedError: "invalid replacement: catchall nodes are not allowed in pattern/replacement ASTs",
+		},
+		{
+			name: "Invalid pattern - Empty TermGroup",
+			pattern: ast.Pattern{
+				Root: &ast.TermGroup{
+					Operands: []ast.Node{},
+					Relation: ast.AndRelation,
+				},
+			},
+			replacement: ast.Replacement{
+				Root: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expectedError: "invalid pattern: empty term group",
+		},
+		{
+			name: "Invalid pattern - Nested CatchallNode",
+			pattern: ast.Pattern{
+				Root: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "DET",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+						&ast.CatchallNode{
+							NodeType: "custom",
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+			replacement: ast.Replacement{
+				Root: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expectedError: "invalid pattern: invalid operand: catchall nodes are not allowed in pattern/replacement ASTs",
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			matcher, err := NewMatcher(tt.pattern, tt.replacement)
+			if tt.expectedError != "" {
+				assert.Error(t, err)
+				assert.Equal(t, tt.expectedError, err.Error())
+				assert.Nil(t, matcher)
+			} else {
+				assert.NoError(t, err)
+				assert.NotNil(t, matcher)
+			}
+		})
+	}
+}
+
+func TestMatchSimplePattern(t *testing.T) {
+	// Create a simple pattern: match a term with DET
+	pattern := ast.Pattern{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	// Create a simple replacement
+	replacement := ast.Replacement{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "COMBINED_DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	m, err := NewMatcher(pattern, replacement)
+	assert.NoError(t, err)
+	assert.NotNil(t, m)
+
+	tests := []struct {
+		name     string
+		input    ast.Node
+		expected bool
+	}{
+		{
+			name: "Exact match",
+			input: &ast.Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   ast.MatchEqual,
+			},
+			expected: true,
+		},
+		{
+			name: "Different key",
+			input: &ast.Term{
+				Foundry: "opennlp",
+				Key:     "NOUN",
+				Layer:   "p",
+				Match:   ast.MatchEqual,
+			},
+			expected: false,
+		},
+		{
+			name: "Different foundry",
+			input: &ast.Term{
+				Foundry: "different",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   ast.MatchEqual,
+			},
+			expected: false,
+		},
+		{
+			name: "Different match type",
+			input: &ast.Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   ast.MatchNotEqual,
+			},
+			expected: false,
+		},
+		{
+			name: "Nested node",
+			input: &ast.Token{
+				Wrap: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expected: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := m.Match(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestMatchComplexPattern(t *testing.T) {
+	// Create a complex pattern: DET AND (AdjType=Pdt OR PronType=Ind)
+	pattern := ast.Pattern{
+		Root: &ast.Token{
+			Wrap: &ast.TermGroup{
+				Operands: []ast.Node{
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "DET",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+					&ast.TermGroup{
+						Operands: []ast.Node{
+							&ast.Term{
+								Foundry: "opennlp",
+								Key:     "AdjType",
+								Layer:   "m",
+								Match:   ast.MatchEqual,
+								Value:   "Pdt",
+							},
+							&ast.Term{
+								Foundry: "opennlp",
+								Key:     "PronType",
+								Layer:   "m",
+								Match:   ast.MatchEqual,
+								Value:   "Ind",
+							},
+						},
+						Relation: ast.OrRelation,
+					},
+				},
+				Relation: ast.AndRelation,
+			},
+		},
+	}
+
+	replacement := ast.Replacement{
+		Root: &ast.Token{
+			Wrap: &ast.Term{
+				Foundry: "opennlp",
+				Key:     "COMBINED_DET",
+				Layer:   "p",
+				Match:   ast.MatchEqual,
+			},
+		},
+	}
+
+	m, err := NewMatcher(pattern, replacement)
+	assert.NoError(t, err)
+	assert.NotNil(t, m)
+
+	tests := []struct {
+		name     string
+		input    ast.Node
+		expected bool
+	}{
+		{
+			name: "Match with AdjType=Pdt",
+			input: &ast.Token{
+				Wrap: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "DET",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "AdjType",
+							Layer:   "m",
+							Match:   ast.MatchEqual,
+							Value:   "Pdt",
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Match with PronType=Ind",
+			input: &ast.Token{
+				Wrap: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "DET",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "PronType",
+							Layer:   "m",
+							Match:   ast.MatchEqual,
+							Value:   "Ind",
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "No match - missing DET",
+			input: &ast.Token{
+				Wrap: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "NOUN",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "AdjType",
+							Layer:   "m",
+							Match:   ast.MatchEqual,
+							Value:   "Pdt",
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "No match - wrong value",
+			input: &ast.Token{
+				Wrap: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "DET",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "AdjType",
+							Layer:   "m",
+							Match:   ast.MatchEqual,
+							Value:   "Wrong",
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+			expected: false,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := m.Match(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestReplace(t *testing.T) {
+	pattern := ast.Pattern{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	replacement := ast.Replacement{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "COMBINED_DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	m, err := NewMatcher(pattern, replacement)
+	assert.NoError(t, err)
+	assert.NotNil(t, m)
+
+	tests := []struct {
+		name     string
+		input    ast.Node
+		expected ast.Node
+	}{
+		{
+			name: "Replace matching pattern",
+			input: &ast.TermGroup{
+				Operands: []ast.Node{
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "DET",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "AdjType",
+						Layer:   "m",
+						Match:   ast.MatchEqual,
+						Value:   "Pdt",
+					},
+				},
+				Relation: ast.AndRelation,
+			},
+			expected: &ast.TermGroup{
+				Operands: []ast.Node{
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "COMBINED_DET",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "AdjType",
+						Layer:   "m",
+						Match:   ast.MatchEqual,
+						Value:   "Pdt",
+					},
+				},
+				Relation: ast.AndRelation,
+			},
+		},
+		{
+			name: "No replacement for non-matching pattern",
+			input: &ast.TermGroup{
+				Operands: []ast.Node{
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "NOUN",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "AdjType",
+						Layer:   "m",
+						Match:   ast.MatchEqual,
+						Value:   "Pdt",
+					},
+				},
+				Relation: ast.AndRelation,
+			},
+			expected: &ast.TermGroup{
+				Operands: []ast.Node{
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "NOUN",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "AdjType",
+						Layer:   "m",
+						Match:   ast.MatchEqual,
+						Value:   "Pdt",
+					},
+				},
+				Relation: ast.AndRelation,
+			},
+		},
+		{
+			name: "Replace in nested structure",
+			input: &ast.Token{
+				Wrap: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.TermGroup{
+							Operands: []ast.Node{
+								&ast.Term{
+									Foundry: "opennlp",
+									Key:     "DET",
+									Layer:   "p",
+									Match:   ast.MatchEqual,
+								},
+								&ast.Term{
+									Foundry: "opennlp",
+									Key:     "AdjType",
+									Layer:   "m",
+									Match:   ast.MatchEqual,
+									Value:   "Pdt",
+								},
+							},
+							Relation: ast.AndRelation,
+						},
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "NOUN",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+			expected: &ast.Token{
+				Wrap: &ast.TermGroup{
+					Operands: []ast.Node{
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "COMBINED_DET",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+						&ast.Term{
+							Foundry: "opennlp",
+							Key:     "NOUN",
+							Layer:   "p",
+							Match:   ast.MatchEqual,
+						},
+					},
+					Relation: ast.AndRelation,
+				},
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := m.Replace(tt.input)
+			assert.Equal(t, tt.expected, result)
+		})
+	}
+}
+
+func TestMatchNodeOrder(t *testing.T) {
+	pattern := ast.Pattern{
+		Root: &ast.TermGroup{
+			Operands: []ast.Node{
+				&ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+				&ast.Term{
+					Foundry: "opennlp",
+					Key:     "AdjType",
+					Layer:   "m",
+					Match:   ast.MatchEqual,
+					Value:   "Pdt",
+				},
+			},
+			Relation: ast.AndRelation,
+		},
+	}
+
+	replacement := ast.Replacement{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "COMBINED_DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	m, err := NewMatcher(pattern, replacement)
+	assert.NoError(t, err)
+	assert.NotNil(t, m)
+
+	// Test with operands in different orders
+	input1 := &ast.TermGroup{
+		Operands: []ast.Node{
+			&ast.Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   ast.MatchEqual,
+			},
+			&ast.Term{
+				Foundry: "opennlp",
+				Key:     "AdjType",
+				Layer:   "m",
+				Match:   ast.MatchEqual,
+				Value:   "Pdt",
+			},
+		},
+		Relation: ast.AndRelation,
+	}
+
+	input2 := &ast.TermGroup{
+		Operands: []ast.Node{
+			&ast.Term{
+				Foundry: "opennlp",
+				Key:     "AdjType",
+				Layer:   "m",
+				Match:   ast.MatchEqual,
+				Value:   "Pdt",
+			},
+			&ast.Term{
+				Foundry: "opennlp",
+				Key:     "DET",
+				Layer:   "p",
+				Match:   ast.MatchEqual,
+			},
+		},
+		Relation: ast.AndRelation,
+	}
+
+	assert.True(t, m.Match(input1), "Should match with original order")
+	assert.True(t, m.Match(input2), "Should match with reversed order")
+}
+
+func TestMatchWithUnknownNodes(t *testing.T) {
+	pattern := ast.Pattern{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	replacement := ast.Replacement{
+		Root: &ast.Term{
+			Foundry: "opennlp",
+			Key:     "COMBINED_DET",
+			Layer:   "p",
+			Match:   ast.MatchEqual,
+		},
+	}
+
+	m, err := NewMatcher(pattern, replacement)
+	assert.NoError(t, err)
+	assert.NotNil(t, m)
+
+	tests := []struct {
+		name     string
+		input    ast.Node
+		expected bool
+	}{
+		{
+			name: "Match term inside unknown node with wrap",
+			input: &ast.CatchallNode{
+				NodeType: "koral:custom",
+				RawContent: json.RawMessage(`{
+					"@type": "koral:custom",
+					"customField": "value"
+				}`),
+				Wrap: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "DET",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Match term inside unknown node's operands",
+			input: &ast.CatchallNode{
+				NodeType: "koral:custom",
+				RawContent: json.RawMessage(`{
+					"@type": "koral:custom",
+					"customField": "value"
+				}`),
+				Operands: []ast.Node{
+					&ast.Term{
+						Foundry: "opennlp",
+						Key:     "DET",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "No match in unknown node with different term",
+			input: &ast.CatchallNode{
+				NodeType: "koral:custom",
+				RawContent: json.RawMessage(`{
+					"@type": "koral:custom",
+					"customField": "value"
+				}`),
+				Wrap: &ast.Term{
+					Foundry: "opennlp",
+					Key:     "NOUN",
+					Layer:   "p",
+					Match:   ast.MatchEqual,
+				},
+			},
+			expected: false,
+		},
+		{
+			name: "Match in deeply nested unknown nodes",
+			input: &ast.CatchallNode{
+				NodeType: "koral:outer",
+				RawContent: json.RawMessage(`{
+					"@type": "koral:outer",
+					"outerField": "value"
+				}`),
+				Wrap: &ast.CatchallNode{
+					NodeType: "koral:inner",
+					RawContent: json.RawMessage(`{
+						"@type": "koral:inner",
+						"innerField": "value"
+					}`),
+					Wrap: &ast.Term{
+						Foundry: "opennlp",
+						Key:     "DET",
+						Layer:   "p",
+						Match:   ast.MatchEqual,
+					},
+				},
+			},
+			expected: true,
+		},
+		{
+			name: "Match in mixed known and unknown nodes",
+			input: &ast.Token{
+				Wrap: &ast.CatchallNode{
+					NodeType: "koral:custom",
+					RawContent: json.RawMessage(`{
+						"@type": "koral:custom",
+						"customField": "value"
+					}`),
+					Operands: []ast.Node{
+						&ast.TermGroup{
+							Operands: []ast.Node{
+								&ast.Term{
+									Foundry: "opennlp",
+									Key:     "DET",
+									Layer:   "p",
+									Match:   ast.MatchEqual,
+								},
+							},
+							Relation: ast.AndRelation,
+						},
+					},
+				},
+			},
+			expected: true,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			result := m.Match(tt.input)
+			assert.Equal(t, tt.expected, result)
+
+			if tt.expected {
+				// Test replacement when there's a match
+				replaced := m.Replace(tt.input)
+				// Verify the replacement happened somewhere in the structure
+				containsReplacement := false
+				var checkNode func(ast.Node)
+				checkNode = func(node ast.Node) {
+					switch n := node.(type) {
+					case *ast.Term:
+						if n.Key == "COMBINED_DET" {
+							containsReplacement = true
+						}
+					case *ast.Token:
+						if n.Wrap != nil {
+							checkNode(n.Wrap)
+						}
+					case *ast.TermGroup:
+						for _, op := range n.Operands {
+							checkNode(op)
+						}
+					case *ast.CatchallNode:
+						if n.Wrap != nil {
+							checkNode(n.Wrap)
+						}
+						for _, op := range n.Operands {
+							checkNode(op)
+						}
+					}
+				}
+				checkNode(replaced)
+				assert.True(t, containsReplacement, "Replacement should be found in the result")
+			}
+		})
+	}
+}