Index: unk/src/org/openstreetmap/josm/actions/search/PushbackTokenizer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/search/PushbackTokenizer.java	(revision 12655)
+++ 	(revision )
@@ -1,350 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.actions.search;
-
-import static org.openstreetmap.josm.tools.I18n.marktr;
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.io.IOException;
-import java.io.Reader;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Objects;
-
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
-import org.openstreetmap.josm.tools.JosmRuntimeException;
-
-/**
- * This class is used to parse a search string and split it into tokens.
- * It provides methods to parse numbers and extract strings.
- */
-public class PushbackTokenizer {
-
-    /**
-     * A range of long numbers. Immutable
-     */
-    public static class Range {
-        private final long start;
-        private final long end;
-
-        /**
-         * Create a new range
-         * @param start The start
-         * @param end The end (inclusive)
-         */
-        public Range(long start, long end) {
-            this.start = start;
-            this.end = end;
-        }
-
-        /**
-         * @return The start
-         */
-        public long getStart() {
-            return start;
-        }
-
-        /**
-         * @return The end (inclusive)
-         */
-        public long getEnd() {
-            return end;
-        }
-
-        @Override
-        public String toString() {
-            return "Range [start=" + start + ", end=" + end + ']';
-        }
-    }
-
-    private final Reader search;
-
-    private Token currentToken;
-    private String currentText;
-    private Long currentNumber;
-    private Long currentRange;
-    private int c;
-    private boolean isRange;
-
-    /**
-     * Creates a new {@link PushbackTokenizer}
-     * @param search The search string reader to read the tokens from
-     */
-    public PushbackTokenizer(Reader search) {
-        this.search = search;
-        getChar();
-    }
-
-    /**
-     * The token types that may be read
-     */
-    public enum Token {
-        /**
-         * Not token (-)
-         */
-        NOT(marktr("<not>")),
-        /**
-         * Or token (or) (|)
-         */
-        OR(marktr("<or>")),
-        /**
-         * Xor token (xor) (^)
-         */
-        XOR(marktr("<xor>")),
-        /**
-         * opening parentheses token (
-         */
-        LEFT_PARENT(marktr("<left parent>")),
-        /**
-         * closing parentheses token )
-         */
-        RIGHT_PARENT(marktr("<right parent>")),
-        /**
-         * Colon :
-         */
-        COLON(marktr("<colon>")),
-        /**
-         * The equals sign (=)
-         */
-        EQUALS(marktr("<equals>")),
-        /**
-         * A text
-         */
-        KEY(marktr("<key>")),
-        /**
-         * A question mark (?)
-         */
-        QUESTION_MARK(marktr("<question mark>")),
-        /**
-         * Marks the end of the input
-         */
-        EOF(marktr("<end-of-file>")),
-        /**
-         * Less than sign (&lt;)
-         */
-        LESS_THAN("<less-than>"),
-        /**
-         * Greater than sign (&gt;)
-         */
-        GREATER_THAN("<greater-than>");
-
-        Token(String name) {
-            this.name = name;
-        }
-
-        private final String name;
-
-        @Override
-        public String toString() {
-            return tr(name);
-        }
-    }
-
-    private void getChar() {
-        try {
-            c = search.read();
-        } catch (IOException e) {
-            throw new JosmRuntimeException(e.getMessage(), e);
-        }
-    }
-
-    private static final List<Character> SPECIAL_CHARS = Arrays.asList('"', ':', '(', ')', '|', '^', '=', '?', '<', '>');
-    private static final List<Character> SPECIAL_CHARS_QUOTED = Arrays.asList('"');
-
-    private String getString(boolean quoted) {
-        List<Character> sChars = quoted ? SPECIAL_CHARS_QUOTED : SPECIAL_CHARS;
-        StringBuilder s = new StringBuilder();
-        boolean escape = false;
-        while (c != -1 && (escape || (!sChars.contains((char) c) && (quoted || !Character.isWhitespace(c))))) {
-            if (c == '\\' && !escape) {
-                escape = true;
-            } else {
-                s.append((char) c);
-                escape = false;
-            }
-            getChar();
-        }
-        return s.toString();
-    }
-
-    private String getString() {
-        return getString(false);
-    }
-
-    /**
-     * The token returned is <code>null</code> or starts with an identifier character:
-     * - for an '-'. This will be the only character
-     * : for an key. The value is the next token
-     * | for "OR"
-     * ^ for "XOR"
-     * ' ' for anything else.
-     * @return The next token in the stream.
-     */
-    public Token nextToken() {
-        if (currentToken != null) {
-            Token result = currentToken;
-            currentToken = null;
-            return result;
-        }
-
-        while (Character.isWhitespace(c)) {
-            getChar();
-        }
-        switch (c) {
-        case -1:
-            getChar();
-            return Token.EOF;
-        case ':':
-            getChar();
-            return Token.COLON;
-        case '=':
-            getChar();
-            return Token.EQUALS;
-        case '<':
-            getChar();
-            return Token.LESS_THAN;
-        case '>':
-            getChar();
-            return Token.GREATER_THAN;
-        case '(':
-            getChar();
-            return Token.LEFT_PARENT;
-        case ')':
-            getChar();
-            return Token.RIGHT_PARENT;
-        case '|':
-            getChar();
-            return Token.OR;
-        case '^':
-            getChar();
-            return Token.XOR;
-        case '&':
-            getChar();
-            return nextToken();
-        case '?':
-            getChar();
-            return Token.QUESTION_MARK;
-        case '"':
-            getChar();
-            currentText = getString(true);
-            getChar();
-            return Token.KEY;
-        default:
-            String prefix = "";
-            if (c == '-') {
-                getChar();
-                if (!Character.isDigit(c))
-                    return Token.NOT;
-                prefix = "-";
-            }
-            currentText = prefix + getString();
-            if ("or".equalsIgnoreCase(currentText))
-                return Token.OR;
-            else if ("xor".equalsIgnoreCase(currentText))
-                return Token.XOR;
-            else if ("and".equalsIgnoreCase(currentText))
-                return nextToken();
-            // try parsing number
-            try {
-                currentNumber = Long.valueOf(currentText);
-            } catch (NumberFormatException e) {
-                currentNumber = null;
-            }
-            // if text contains "-", try parsing a range
-            int pos = currentText.indexOf('-', 1);
-            isRange = pos > 0;
-            if (isRange) {
-                try {
-                    currentNumber = Long.valueOf(currentText.substring(0, pos));
-                } catch (NumberFormatException e) {
-                    currentNumber = null;
-                }
-                try {
-                    currentRange = Long.valueOf(currentText.substring(pos + 1));
-                } catch (NumberFormatException e) {
-                    currentRange = null;
-                    }
-                } else {
-                    currentRange = null;
-                }
-            return Token.KEY;
-        }
-    }
-
-    /**
-     * Reads the next token if it is equal to the given, suggested token
-     * @param token The token the next one should be equal to
-     * @return <code>true</code> if it has been read
-     */
-    public boolean readIfEqual(Token token) {
-        Token nextTok = nextToken();
-        if (Objects.equals(nextTok, token))
-            return true;
-        currentToken = nextTok;
-        return false;
-    }
-
-    /**
-     * Reads the next token. If it is a text, return that text. If not, advance
-     * @return the text or <code>null</code> if the reader was advanced
-     */
-    public String readTextOrNumber() {
-        Token nextTok = nextToken();
-        if (nextTok == Token.KEY)
-            return currentText;
-        currentToken = nextTok;
-        return null;
-    }
-
-    /**
-     * Reads a number
-     * @param errorMessage The error if the number cannot be read
-     * @return The number that was found
-     * @throws ParseError if there is no number
-     */
-    public long readNumber(String errorMessage) throws ParseError {
-        if ((nextToken() == Token.KEY) && (currentNumber != null))
-            return currentNumber;
-        else
-            throw new ParseError(errorMessage);
-    }
-
-    /**
-     * Gets the last number that was read
-     * @return The last number
-     */
-    public long getReadNumber() {
-        return (currentNumber != null) ? currentNumber : 0;
-    }
-
-    /**
-     * Reads a range of numbers
-     * @param errorMessage The error if the input is malformed
-     * @return The range that was found
-     * @throws ParseError If the input is not as expected for a range
-     */
-    public Range readRange(String errorMessage) throws ParseError {
-        if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) {
-            throw new ParseError(errorMessage);
-        } else if (!isRange && currentNumber != null) {
-            if (currentNumber >= 0) {
-                return new Range(currentNumber, currentNumber);
-            } else {
-                return new Range(0, Math.abs(currentNumber));
-            }
-        } else if (isRange && currentRange == null) {
-            return new Range(currentNumber, Long.MAX_VALUE);
-        } else if (currentNumber != null && currentRange != null) {
-            return new Range(currentNumber, currentRange);
-        } else {
-            throw new ParseError(errorMessage);
-        }
-    }
-
-    /**
-     * Gets the last text that was found
-     * @return The text
-     */
-    public String getText() {
-        return currentText;
-    }
-}
Index: /trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/actions/search/SearchAction.java	(revision 12656)
@@ -46,8 +46,9 @@
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.actions.ParameterizedAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Filter;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -380,5 +381,5 @@
                     SearchCompiler.compile(ss);
                     return true;
-                } catch (ParseError | MapCSSException e) {
+                } catch (SearchParseError | MapCSSException e) {
                     return false;
                 }
@@ -418,5 +419,5 @@
                         SearchCompiler.compile(ss);
                         super.buttonAction(buttonIndex, evt);
-                    } catch (ParseError e) {
+                    } catch (SearchParseError e) {
                         Logging.debug(e);
                         JOptionPane.showMessageDialog(
@@ -828,5 +829,5 @@
                 }
                 subMonitor.finishTask();
-            } catch (ParseError e) {
+            } catch (SearchParseError e) {
                 Logging.debug(e);
                 JOptionPane.showMessageDialog(
Index: unk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/actions/search/SearchCompiler.java	(revision 12655)
+++ 	(revision )
@@ -1,1876 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.actions.search;
-
-import static org.openstreetmap.josm.tools.I18n.marktr;
-import static org.openstreetmap.josm.tools.I18n.tr;
-
-import java.io.PushbackReader;
-import java.io.StringReader;
-import java.text.Normalizer;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Optional;
-import java.util.function.Predicate;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
-import java.util.regex.PatternSyntaxException;
-import java.util.stream.Collectors;
-
-import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.search.PushbackTokenizer.Range;
-import org.openstreetmap.josm.actions.search.PushbackTokenizer.Token;
-import org.openstreetmap.josm.data.Bounds;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
-import org.openstreetmap.josm.data.osm.OsmUtils;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.data.osm.Tagged;
-import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.gui.MainApplication;
-import org.openstreetmap.josm.gui.mappaint.Environment;
-import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
-import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
-import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSeparator;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.tools.AlphanumComparator;
-import org.openstreetmap.josm.tools.Geometry;
-import org.openstreetmap.josm.tools.Logging;
-import org.openstreetmap.josm.tools.UncheckedParseException;
-import org.openstreetmap.josm.tools.Utils;
-import org.openstreetmap.josm.tools.date.DateUtils;
-
-/**
- Implements a google-like search.
- <br>
- Grammar:
-<pre>
-expression =
-  fact | expression
-  fact expression
-  fact
-
-fact =
- ( expression )
- -fact
- term?
- term=term
- term:term
- term
- </pre>
-
- @author Imi
- */
-public class SearchCompiler {
-
-    private final boolean caseSensitive;
-    private final boolean regexSearch;
-    private static String rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
-    private static String rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}");
-    private final PushbackTokenizer tokenizer;
-    private static Map<String, SimpleMatchFactory> simpleMatchFactoryMap = new HashMap<>();
-    private static Map<String, UnaryMatchFactory> unaryMatchFactoryMap = new HashMap<>();
-    private static Map<String, BinaryMatchFactory> binaryMatchFactoryMap = new HashMap<>();
-
-    public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) {
-        this.caseSensitive = caseSensitive;
-        this.regexSearch = regexSearch;
-        this.tokenizer = tokenizer;
-
-        // register core match factories at first instance, so plugins should never be able to generate a NPE
-        if (simpleMatchFactoryMap.isEmpty()) {
-            addMatchFactory(new CoreSimpleMatchFactory());
-        }
-        if (unaryMatchFactoryMap.isEmpty()) {
-            addMatchFactory(new CoreUnaryMatchFactory());
-        }
-    }
-
-    /**
-     * Add (register) MatchFactory with SearchCompiler
-     * @param factory match factory
-     */
-    public static void addMatchFactory(MatchFactory factory) {
-        for (String keyword : factory.getKeywords()) {
-            final MatchFactory existing;
-            if (factory instanceof SimpleMatchFactory) {
-                existing = simpleMatchFactoryMap.put(keyword, (SimpleMatchFactory) factory);
-            } else if (factory instanceof UnaryMatchFactory) {
-                existing = unaryMatchFactoryMap.put(keyword, (UnaryMatchFactory) factory);
-            } else if (factory instanceof BinaryMatchFactory) {
-                existing = binaryMatchFactoryMap.put(keyword, (BinaryMatchFactory) factory);
-            } else
-                throw new AssertionError("Unknown match factory");
-            if (existing != null) {
-                Logging.warn("SearchCompiler: for key ''{0}'', overriding match factory ''{1}'' with ''{2}''", keyword, existing, factory);
-            }
-        }
-    }
-
-    public class CoreSimpleMatchFactory implements SimpleMatchFactory {
-        private final Collection<String> keywords = Arrays.asList("id", "version", "type", "user", "role",
-                "changeset", "nodes", "ways", "tags", "areasize", "waylength", "modified", "deleted", "selected",
-                "incomplete", "untagged", "closed", "new", "indownloadedarea",
-                "allindownloadedarea", "inview", "allinview", "timestamp", "nth", "nth%", "hasRole", "preset");
-
-        @Override
-        public Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError {
-            switch(keyword) {
-            case "modified":
-                return new Modified();
-            case "deleted":
-                return new Deleted();
-            case "selected":
-                return new Selected();
-            case "incomplete":
-                return new Incomplete();
-            case "untagged":
-                return new Untagged();
-            case "closed":
-                return new Closed();
-            case "new":
-                return new New();
-            case "indownloadedarea":
-                return new InDataSourceArea(false);
-            case "allindownloadedarea":
-                return new InDataSourceArea(true);
-            case "inview":
-                return new InView(false);
-            case "allinview":
-                return new InView(true);
-            default:
-                if (tokenizer != null) {
-                    switch (keyword) {
-                    case "id":
-                        return new Id(tokenizer);
-                    case "version":
-                        return new Version(tokenizer);
-                    case "type":
-                        return new ExactType(tokenizer.readTextOrNumber());
-                    case "preset":
-                        return new Preset(tokenizer.readTextOrNumber());
-                    case "user":
-                        return new UserMatch(tokenizer.readTextOrNumber());
-                    case "role":
-                        return new RoleMatch(tokenizer.readTextOrNumber());
-                    case "changeset":
-                        return new ChangesetId(tokenizer);
-                    case "nodes":
-                        return new NodeCountRange(tokenizer);
-                    case "ways":
-                        return new WayCountRange(tokenizer);
-                    case "tags":
-                        return new TagCountRange(tokenizer);
-                    case "areasize":
-                        return new AreaSize(tokenizer);
-                    case "waylength":
-                        return new WayLength(tokenizer);
-                    case "nth":
-                        return new Nth(tokenizer, false);
-                    case "nth%":
-                        return new Nth(tokenizer, true);
-                    case "hasRole":
-                        return new HasRole(tokenizer);
-                    case "timestamp":
-                        // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
-                        String rangeS = ' ' + tokenizer.readTextOrNumber() + ' ';
-                        String[] rangeA = rangeS.split("/");
-                        if (rangeA.length == 1) {
-                            return new KeyValue(keyword, rangeS.trim(), regexSearch, caseSensitive);
-                        } else if (rangeA.length == 2) {
-                            String rangeA1 = rangeA[0].trim();
-                            String rangeA2 = rangeA[1].trim();
-                            final long minDate;
-                            final long maxDate;
-                            try {
-                                // if min timestap is empty: use lowest possible date
-                                minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime();
-                            } catch (UncheckedParseException ex) {
-                                throw new ParseError(tr("Cannot parse timestamp ''{0}''", rangeA1), ex);
-                            }
-                            try {
-                                // if max timestamp is empty: use "now"
-                                maxDate = rangeA2.isEmpty() ? System.currentTimeMillis() : DateUtils.fromString(rangeA2).getTime();
-                            } catch (UncheckedParseException ex) {
-                                throw new ParseError(tr("Cannot parse timestamp ''{0}''", rangeA2), ex);
-                            }
-                            return new TimestampRange(minDate, maxDate);
-                        } else {
-                            throw new ParseError("<html>" + tr("Expecting {0} after {1}", "<i>min</i>/<i>max</i>", "<i>timestamp</i>"));
-                        }
-                    }
-                } else {
-                    throw new ParseError("<html>" + tr("Expecting {0} after {1}", "<code>:</code>", "<i>" + keyword + "</i>"));
-                }
-            }
-            throw new IllegalStateException("Not expecting keyword " + keyword);
-        }
-
-        @Override
-        public Collection<String> getKeywords() {
-            return keywords;
-        }
-    }
-
-    public static class CoreUnaryMatchFactory implements UnaryMatchFactory {
-        private static Collection<String> keywords = Arrays.asList("parent", "child");
-
-        @Override
-        public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) {
-            if ("parent".equals(keyword))
-                return new Parent(matchOperand);
-            else if ("child".equals(keyword))
-                return new Child(matchOperand);
-            return null;
-        }
-
-        @Override
-        public Collection<String> getKeywords() {
-            return keywords;
-        }
-    }
-
-    /**
-     * Classes implementing this interface can provide Match operators.
-     * @since 10600 (functional interface)
-     */
-    @FunctionalInterface
-    private interface MatchFactory {
-        Collection<String> getKeywords();
-    }
-
-    public interface SimpleMatchFactory extends MatchFactory {
-        Match get(String keyword, PushbackTokenizer tokenizer) throws ParseError;
-    }
-
-    public interface UnaryMatchFactory extends MatchFactory {
-        UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws ParseError;
-    }
-
-    public interface BinaryMatchFactory extends MatchFactory {
-        AbstractBinaryMatch get(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws ParseError;
-    }
-
-    /**
-     * Base class for all search criteria. If the criterion only depends on an object's tags,
-     * inherit from {@link org.openstreetmap.josm.actions.search.SearchCompiler.TaggedMatch}.
-     */
-    public abstract static class Match implements Predicate<OsmPrimitive> {
-
-        /**
-         * Tests whether the primitive matches this criterion.
-         * @param osm the primitive to test
-         * @return true if the primitive matches this criterion
-         */
-        public abstract boolean match(OsmPrimitive osm);
-
-        /**
-         * Tests whether the tagged object matches this criterion.
-         * @param tagged the tagged object to test
-         * @return true if the tagged object matches this criterion
-         */
-        public boolean match(Tagged tagged) {
-            return false;
-        }
-
-        @Override
-        public final boolean test(OsmPrimitive object) {
-            return match(object);
-        }
-    }
-
-    public abstract static class TaggedMatch extends Match {
-
-        @Override
-        public abstract boolean match(Tagged tags);
-
-        @Override
-        public final boolean match(OsmPrimitive osm) {
-            return match((Tagged) osm);
-        }
-    }
-
-    /**
-     * A unary search operator which may take data parameters.
-     */
-    public abstract static class UnaryMatch extends Match {
-
-        protected final Match match;
-
-        public UnaryMatch(Match match) {
-            if (match == null) {
-                // "operator" (null) should mean the same as "operator()"
-                // (Always). I.e. match everything
-                this.match = Always.INSTANCE;
-            } else {
-                this.match = match;
-            }
-        }
-
-        public Match getOperand() {
-            return match;
-        }
-    }
-
-    /**
-     * A binary search operator which may take data parameters.
-     */
-    public abstract static class AbstractBinaryMatch extends Match {
-
-        protected final Match lhs;
-        protected final Match rhs;
-
-        /**
-         * Constructs a new {@code BinaryMatch}.
-         * @param lhs Left hand side
-         * @param rhs Right hand side
-         */
-        public AbstractBinaryMatch(Match lhs, Match rhs) {
-            this.lhs = lhs;
-            this.rhs = rhs;
-        }
-
-        /**
-         * Returns left hand side.
-         * @return left hand side
-         */
-        public final Match getLhs() {
-            return lhs;
-        }
-
-        /**
-         * Returns right hand side.
-         * @return right hand side
-         */
-        public final Match getRhs() {
-            return rhs;
-        }
-
-        protected static String parenthesis(Match m) {
-            return '(' + m.toString() + ')';
-        }
-    }
-
-    /**
-     * Matches every OsmPrimitive.
-     */
-    public static class Always extends TaggedMatch {
-        /** The unique instance/ */
-        public static final Always INSTANCE = new Always();
-        @Override
-        public boolean match(Tagged osm) {
-            return true;
-        }
-    }
-
-    /**
-     * Never matches any OsmPrimitive.
-     */
-    public static class Never extends TaggedMatch {
-        /** The unique instance/ */
-        public static final Never INSTANCE = new Never();
-        @Override
-        public boolean match(Tagged osm) {
-            return false;
-        }
-    }
-
-    /**
-     * Inverts the match.
-     */
-    public static class Not extends UnaryMatch {
-        public Not(Match match) {
-            super(match);
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return !match.match(osm);
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            return !match.match(osm);
-        }
-
-        @Override
-        public String toString() {
-            return '!' + match.toString();
-        }
-
-        public Match getMatch() {
-            return match;
-        }
-    }
-
-    /**
-     * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
-     */
-    private static class BooleanMatch extends TaggedMatch {
-        private final String key;
-        private final boolean defaultValue;
-
-        BooleanMatch(String key, boolean defaultValue) {
-            this.key = key;
-            this.defaultValue = defaultValue;
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            return Optional.ofNullable(OsmUtils.getOsmBoolean(osm.get(key))).orElse(defaultValue);
-        }
-
-        @Override
-        public String toString() {
-            return key + '?';
-        }
-    }
-
-    /**
-     * Matches if both left and right expressions match.
-     */
-    public static class And extends AbstractBinaryMatch {
-        /**
-         * Constructs a new {@code And} match.
-         * @param lhs left hand side
-         * @param rhs right hand side
-         */
-        public And(Match lhs, Match rhs) {
-            super(lhs, rhs);
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return lhs.match(osm) && rhs.match(osm);
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            return lhs.match(osm) && rhs.match(osm);
-        }
-
-        @Override
-        public String toString() {
-            return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof And) ? parenthesis(lhs) : lhs) + " && "
-                 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof And) ? parenthesis(rhs) : rhs);
-        }
-    }
-
-    /**
-     * Matches if the left OR the right expression match.
-     */
-    public static class Or extends AbstractBinaryMatch {
-        /**
-         * Constructs a new {@code Or} match.
-         * @param lhs left hand side
-         * @param rhs right hand side
-         */
-        public Or(Match lhs, Match rhs) {
-            super(lhs, rhs);
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return lhs.match(osm) || rhs.match(osm);
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            return lhs.match(osm) || rhs.match(osm);
-        }
-
-        @Override
-        public String toString() {
-            return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof Or) ? parenthesis(lhs) : lhs) + " || "
-                 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof Or) ? parenthesis(rhs) : rhs);
-        }
-    }
-
-    /**
-     * Matches if the left OR the right expression match, but not both.
-     */
-    public static class Xor extends AbstractBinaryMatch {
-        /**
-         * Constructs a new {@code Xor} match.
-         * @param lhs left hand side
-         * @param rhs right hand side
-         */
-        public Xor(Match lhs, Match rhs) {
-            super(lhs, rhs);
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return lhs.match(osm) ^ rhs.match(osm);
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            return lhs.match(osm) ^ rhs.match(osm);
-        }
-
-        @Override
-        public String toString() {
-            return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof Xor) ? parenthesis(lhs) : lhs) + " ^ "
-                 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof Xor) ? parenthesis(rhs) : rhs);
-        }
-    }
-
-    /**
-     * Matches objects with ID in the given range.
-     */
-    private static class Id extends RangeMatch {
-        Id(Range range) {
-            super(range);
-        }
-
-        Id(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of primitive ids expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            return osm.isNew() ? 0 : osm.getUniqueId();
-        }
-
-        @Override
-        protected String getString() {
-            return "id";
-        }
-    }
-
-    /**
-     * Matches objects with a changeset ID in the given range.
-     */
-    private static class ChangesetId extends RangeMatch {
-        ChangesetId(Range range) {
-            super(range);
-        }
-
-        ChangesetId(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of changeset ids expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            return (long) osm.getChangesetId();
-        }
-
-        @Override
-        protected String getString() {
-            return "changeset";
-        }
-    }
-
-    /**
-     * Matches objects with a version number in the given range.
-     */
-    private static class Version extends RangeMatch {
-        Version(Range range) {
-            super(range);
-        }
-
-        Version(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of versions expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            return (long) osm.getVersion();
-        }
-
-        @Override
-        protected String getString() {
-            return "version";
-        }
-    }
-
-    /**
-     * Matches objects with the given key-value pair.
-     */
-    private static class KeyValue extends TaggedMatch {
-        private final String key;
-        private final Pattern keyPattern;
-        private final String value;
-        private final Pattern valuePattern;
-        private final boolean caseSensitive;
-
-        KeyValue(String key, String value, boolean regexSearch, boolean caseSensitive) throws ParseError {
-            this.caseSensitive = caseSensitive;
-            if (regexSearch) {
-                int searchFlags = regexFlags(caseSensitive);
-
-                try {
-                    this.keyPattern = Pattern.compile(key, searchFlags);
-                } catch (PatternSyntaxException e) {
-                    throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
-                } catch (IllegalArgumentException e) {
-                    throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage()), e);
-                }
-                try {
-                    this.valuePattern = Pattern.compile(value, searchFlags);
-                } catch (PatternSyntaxException e) {
-                    throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
-                } catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
-                    throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage()), e);
-                }
-                this.key = key;
-                this.value = value;
-
-            } else {
-                this.key = key;
-                this.value = value;
-                this.keyPattern = null;
-                this.valuePattern = null;
-            }
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-
-            if (keyPattern != null) {
-                if (!osm.hasKeys())
-                    return false;
-
-                /* The string search will just get a key like
-                 * 'highway' and look that up as osm.get(key). But
-                 * since we're doing a regex match we'll have to loop
-                 * over all the keys to see if they match our regex,
-                 * and only then try to match against the value
-                 */
-
-                for (String k: osm.keySet()) {
-                    String v = osm.get(k);
-
-                    Matcher matcherKey = keyPattern.matcher(k);
-                    boolean matchedKey = matcherKey.find();
-
-                    if (matchedKey) {
-                        Matcher matcherValue = valuePattern.matcher(v);
-                        boolean matchedValue = matcherValue.find();
-
-                        if (matchedValue)
-                            return true;
-                    }
-                }
-            } else {
-                String mv;
-
-                if ("timestamp".equals(key) && osm instanceof OsmPrimitive) {
-                    mv = DateUtils.fromTimestamp(((OsmPrimitive) osm).getRawTimestamp());
-                } else {
-                    mv = osm.get(key);
-                    if (!caseSensitive && mv == null) {
-                        for (String k: osm.keySet()) {
-                            if (key.equalsIgnoreCase(k)) {
-                                mv = osm.get(k);
-                                break;
-                            }
-                        }
-                    }
-                }
-
-                if (mv == null)
-                    return false;
-
-                String v1 = caseSensitive ? mv : mv.toLowerCase(Locale.ENGLISH);
-                String v2 = caseSensitive ? value : value.toLowerCase(Locale.ENGLISH);
-
-                v1 = Normalizer.normalize(v1, Normalizer.Form.NFC);
-                v2 = Normalizer.normalize(v2, Normalizer.Form.NFC);
-                return v1.indexOf(v2) != -1;
-            }
-
-            return false;
-        }
-
-        @Override
-        public String toString() {
-            return key + '=' + value;
-        }
-    }
-
-    public static class ValueComparison extends TaggedMatch {
-        private final String key;
-        private final String referenceValue;
-        private final Double referenceNumber;
-        private final int compareMode;
-        private static final Pattern ISO8601 = Pattern.compile("\\d+-\\d+-\\d+");
-
-        public ValueComparison(String key, String referenceValue, int compareMode) {
-            this.key = key;
-            this.referenceValue = referenceValue;
-            Double v = null;
-            try {
-                if (referenceValue != null) {
-                    v = Double.valueOf(referenceValue);
-                }
-            } catch (NumberFormatException ignore) {
-                Logging.trace(ignore);
-            }
-            this.referenceNumber = v;
-            this.compareMode = compareMode;
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            final String currentValue = osm.get(key);
-            final int compareResult;
-            if (currentValue == null) {
-                return false;
-            } else if (ISO8601.matcher(currentValue).matches() || ISO8601.matcher(referenceValue).matches()) {
-                compareResult = currentValue.compareTo(referenceValue);
-            } else if (referenceNumber != null) {
-                try {
-                    compareResult = Double.compare(Double.parseDouble(currentValue), referenceNumber);
-                } catch (NumberFormatException ignore) {
-                    return false;
-                }
-            } else {
-                compareResult = AlphanumComparator.getInstance().compare(currentValue, referenceValue);
-            }
-            return compareMode < 0 ? compareResult < 0 : compareMode > 0 ? compareResult > 0 : compareResult == 0;
-        }
-
-        @Override
-        public String toString() {
-            return key + (compareMode == -1 ? "<" : compareMode == +1 ? ">" : "") + referenceValue;
-        }
-    }
-
-    /**
-     * Matches objects with the exact given key-value pair.
-     */
-    public static class ExactKeyValue extends TaggedMatch {
-
-        enum Mode {
-            ANY, ANY_KEY, ANY_VALUE, EXACT, NONE, MISSING_KEY,
-            ANY_KEY_REGEXP, ANY_VALUE_REGEXP, EXACT_REGEXP, MISSING_KEY_REGEXP;
-        }
-
-        private final String key;
-        private final String value;
-        private final Pattern keyPattern;
-        private final Pattern valuePattern;
-        private final Mode mode;
-
-        /**
-         * Constructs a new {@code ExactKeyValue}.
-         * @param regexp regular expression
-         * @param key key
-         * @param value value
-         * @throws ParseError if a parse error occurs
-         */
-        public ExactKeyValue(boolean regexp, String key, String value) throws ParseError {
-            if ("".equals(key))
-                throw new ParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
-            this.key = key;
-            this.value = value == null ? "" : value;
-            if ("".equals(this.value) && "*".equals(key)) {
-                mode = Mode.NONE;
-            } else if ("".equals(this.value)) {
-                if (regexp) {
-                    mode = Mode.MISSING_KEY_REGEXP;
-                } else {
-                    mode = Mode.MISSING_KEY;
-                }
-            } else if ("*".equals(key) && "*".equals(this.value)) {
-                mode = Mode.ANY;
-            } else if ("*".equals(key)) {
-                if (regexp) {
-                    mode = Mode.ANY_KEY_REGEXP;
-                } else {
-                    mode = Mode.ANY_KEY;
-                }
-            } else if ("*".equals(this.value)) {
-                if (regexp) {
-                    mode = Mode.ANY_VALUE_REGEXP;
-                } else {
-                    mode = Mode.ANY_VALUE;
-                }
-            } else {
-                if (regexp) {
-                    mode = Mode.EXACT_REGEXP;
-                } else {
-                    mode = Mode.EXACT;
-                }
-            }
-
-            if (regexp && !key.isEmpty() && !"*".equals(key)) {
-                try {
-                    keyPattern = Pattern.compile(key, regexFlags(false));
-                } catch (PatternSyntaxException e) {
-                    throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
-                } catch (IllegalArgumentException e) {
-                    throw new ParseError(tr(rxErrorMsgNoPos, key, e.getMessage()), e);
-                }
-            } else {
-                keyPattern = null;
-            }
-            if (regexp && !this.value.isEmpty() && !"*".equals(this.value)) {
-                try {
-                    valuePattern = Pattern.compile(this.value, regexFlags(false));
-                } catch (PatternSyntaxException e) {
-                    throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
-                } catch (IllegalArgumentException e) {
-                    throw new ParseError(tr(rxErrorMsgNoPos, value, e.getMessage()), e);
-                }
-            } else {
-                valuePattern = null;
-            }
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-
-            if (!osm.hasKeys())
-                return mode == Mode.NONE;
-
-            switch (mode) {
-            case NONE:
-                return false;
-            case MISSING_KEY:
-                return osm.get(key) == null;
-            case ANY:
-                return true;
-            case ANY_VALUE:
-                return osm.get(key) != null;
-            case ANY_KEY:
-                for (String v:osm.getKeys().values()) {
-                    if (v.equals(value))
-                        return true;
-                }
-                return false;
-            case EXACT:
-                return value.equals(osm.get(key));
-            case ANY_KEY_REGEXP:
-                for (String v:osm.getKeys().values()) {
-                    if (valuePattern.matcher(v).matches())
-                        return true;
-                }
-                return false;
-            case ANY_VALUE_REGEXP:
-            case EXACT_REGEXP:
-                for (String k : osm.keySet()) {
-                    if (keyPattern.matcher(k).matches()
-                            && (mode == Mode.ANY_VALUE_REGEXP || valuePattern.matcher(osm.get(k)).matches()))
-                        return true;
-                }
-                return false;
-            case MISSING_KEY_REGEXP:
-                for (String k:osm.keySet()) {
-                    if (keyPattern.matcher(k).matches())
-                        return false;
-                }
-                return true;
-            }
-            throw new AssertionError("Missed state");
-        }
-
-        @Override
-        public String toString() {
-            return key + '=' + value;
-        }
-    }
-
-    /**
-     * Match a string in any tags (key or value), with optional regex and case insensitivity.
-     */
-    private static class Any extends TaggedMatch {
-        private final String search;
-        private final Pattern searchRegex;
-        private final boolean caseSensitive;
-
-        Any(String s, boolean regexSearch, boolean caseSensitive) throws ParseError {
-            s = Normalizer.normalize(s, Normalizer.Form.NFC);
-            this.caseSensitive = caseSensitive;
-            if (regexSearch) {
-                try {
-                    this.searchRegex = Pattern.compile(s, regexFlags(caseSensitive));
-                } catch (PatternSyntaxException e) {
-                    throw new ParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
-                } catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
-                    // StringIndexOutOfBoundsException catched because of https://bugs.openjdk.java.net/browse/JI-9044959
-                    // See #13870: To remove after we switch to a version of Java which resolves this bug
-                    throw new ParseError(tr(rxErrorMsgNoPos, s, e.getMessage()), e);
-                }
-                this.search = s;
-            } else if (caseSensitive) {
-                this.search = s;
-                this.searchRegex = null;
-            } else {
-                this.search = s.toLowerCase(Locale.ENGLISH);
-                this.searchRegex = null;
-            }
-        }
-
-        @Override
-        public boolean match(Tagged osm) {
-            if (!osm.hasKeys())
-                return search.isEmpty();
-
-            for (String key: osm.keySet()) {
-                String value = osm.get(key);
-                if (searchRegex != null) {
-
-                    value = Normalizer.normalize(value, Normalizer.Form.NFC);
-
-                    Matcher keyMatcher = searchRegex.matcher(key);
-                    Matcher valMatcher = searchRegex.matcher(value);
-
-                    boolean keyMatchFound = keyMatcher.find();
-                    boolean valMatchFound = valMatcher.find();
-
-                    if (keyMatchFound || valMatchFound)
-                        return true;
-                } else {
-                    if (!caseSensitive) {
-                        key = key.toLowerCase(Locale.ENGLISH);
-                        value = value.toLowerCase(Locale.ENGLISH);
-                    }
-
-                    value = Normalizer.normalize(value, Normalizer.Form.NFC);
-
-                    if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
-                        return true;
-                }
-            }
-            return false;
-        }
-
-        @Override
-        public String toString() {
-            return search;
-        }
-    }
-
-    private static class ExactType extends Match {
-        private final OsmPrimitiveType type;
-
-        ExactType(String type) throws ParseError {
-            this.type = OsmPrimitiveType.from(type);
-            if (this.type == null)
-                throw new ParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation", type));
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return type.equals(osm.getType());
-        }
-
-        @Override
-        public String toString() {
-            return "type=" + type;
-        }
-    }
-
-    /**
-     * Matches objects last changed by the given username.
-     */
-    private static class UserMatch extends Match {
-        private String user;
-
-        UserMatch(String user) {
-            if ("anonymous".equals(user)) {
-                this.user = null;
-            } else {
-                this.user = user;
-            }
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            if (osm.getUser() == null)
-                return user == null;
-            else
-                return osm.getUser().hasName(user);
-        }
-
-        @Override
-        public String toString() {
-            return "user=" + (user == null ? "" : user);
-        }
-    }
-
-    /**
-     * Matches objects with the given relation role (i.e. "outer").
-     */
-    private static class RoleMatch extends Match {
-        private String role;
-
-        RoleMatch(String role) {
-            if (role == null) {
-                this.role = "";
-            } else {
-                this.role = role;
-            }
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            for (OsmPrimitive ref: osm.getReferrers()) {
-                if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
-                    for (RelationMember m : ((Relation) ref).getMembers()) {
-                        if (m.getMember() == osm) {
-                            String testRole = m.getRole();
-                            if (role.equals(testRole == null ? "" : testRole))
-                                return true;
-                        }
-                    }
-                }
-            }
-            return false;
-        }
-
-        @Override
-        public String toString() {
-            return "role=" + role;
-        }
-    }
-
-    /**
-     * Matches the n-th object of a relation and/or the n-th node of a way.
-     */
-    private static class Nth extends Match {
-
-        private final int nth;
-        private final boolean modulo;
-
-        Nth(PushbackTokenizer tokenizer, boolean modulo) throws ParseError {
-            this((int) tokenizer.readNumber(tr("Positive integer expected")), modulo);
-        }
-
-        private Nth(int nth, boolean modulo) {
-            this.nth = nth;
-            this.modulo = modulo;
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            for (OsmPrimitive p : osm.getReferrers()) {
-                final int idx;
-                final int maxIndex;
-                if (p instanceof Way) {
-                    Way w = (Way) p;
-                    idx = w.getNodes().indexOf(osm);
-                    maxIndex = w.getNodesCount();
-                } else if (p instanceof Relation) {
-                    Relation r = (Relation) p;
-                    idx = r.getMemberPrimitivesList().indexOf(osm);
-                    maxIndex = r.getMembersCount();
-                } else {
-                    continue;
-                }
-                if (nth < 0 && idx - maxIndex == nth) {
-                    return true;
-                } else if (idx == nth || (modulo && idx % nth == 0))
-                    return true;
-            }
-            return false;
-        }
-
-        @Override
-        public String toString() {
-            return "Nth{nth=" + nth + ", modulo=" + modulo + '}';
-        }
-    }
-
-    /**
-     * Matches objects with properties in a certain range.
-     */
-    private abstract static class RangeMatch extends Match {
-
-        private final long min;
-        private final long max;
-
-        RangeMatch(long min, long max) {
-            this.min = Math.min(min, max);
-            this.max = Math.max(min, max);
-        }
-
-        RangeMatch(Range range) {
-            this(range.getStart(), range.getEnd());
-        }
-
-        protected abstract Long getNumber(OsmPrimitive osm);
-
-        protected abstract String getString();
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            Long num = getNumber(osm);
-            if (num == null)
-                return false;
-            else
-                return (num >= min) && (num <= max);
-        }
-
-        @Override
-        public String toString() {
-            return getString() + '=' + min + '-' + max;
-        }
-    }
-
-    /**
-     * Matches ways with a number of nodes in given range
-     */
-    private static class NodeCountRange extends RangeMatch {
-        NodeCountRange(Range range) {
-            super(range);
-        }
-
-        NodeCountRange(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of numbers expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            if (osm instanceof Way) {
-                return (long) ((Way) osm).getRealNodesCount();
-            } else if (osm instanceof Relation) {
-                return (long) ((Relation) osm).getMemberPrimitives(Node.class).size();
-            } else {
-                return null;
-            }
-        }
-
-        @Override
-        protected String getString() {
-            return "nodes";
-        }
-    }
-
-    /**
-     * Matches objects with the number of referring/contained ways in the given range
-     */
-    private static class WayCountRange extends RangeMatch {
-        WayCountRange(Range range) {
-            super(range);
-        }
-
-        WayCountRange(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of numbers expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            if (osm instanceof Node) {
-                return (long) Utils.filteredCollection(osm.getReferrers(), Way.class).size();
-            } else if (osm instanceof Relation) {
-                return (long) ((Relation) osm).getMemberPrimitives(Way.class).size();
-            } else {
-                return null;
-            }
-        }
-
-        @Override
-        protected String getString() {
-            return "ways";
-        }
-    }
-
-    /**
-     * Matches objects with a number of tags in given range
-     */
-    private static class TagCountRange extends RangeMatch {
-        TagCountRange(Range range) {
-            super(range);
-        }
-
-        TagCountRange(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of numbers expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            return (long) osm.getKeys().size();
-        }
-
-        @Override
-        protected String getString() {
-            return "tags";
-        }
-    }
-
-    /**
-     * Matches objects with a timestamp in given range
-     */
-    private static class TimestampRange extends RangeMatch {
-
-        TimestampRange(long minCount, long maxCount) {
-            super(minCount, maxCount);
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            return osm.getTimestamp().getTime();
-        }
-
-        @Override
-        protected String getString() {
-            return "timestamp";
-        }
-    }
-
-    /**
-     * Matches relations with a member of the given role
-     */
-    private static class HasRole extends Match {
-        private final String role;
-
-        HasRole(PushbackTokenizer tokenizer) {
-            role = tokenizer.readTextOrNumber();
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm instanceof Relation && ((Relation) osm).getMemberRoles().contains(role);
-        }
-    }
-
-    /**
-     * Matches objects that are new (i.e. have not been uploaded to the server)
-     */
-    private static class New extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm.isNew();
-        }
-
-        @Override
-        public String toString() {
-            return "new";
-        }
-    }
-
-    /**
-     * Matches all objects that have been modified, created, or undeleted
-     */
-    private static class Modified extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm.isModified() || osm.isNewOrUndeleted();
-        }
-
-        @Override
-        public String toString() {
-            return "modified";
-        }
-    }
-
-    /**
-     * Matches all objects that have been deleted
-     */
-    private static class Deleted extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm.isDeleted();
-        }
-
-        @Override
-        public String toString() {
-            return "deleted";
-        }
-    }
-
-    /**
-     * Matches all objects currently selected
-     */
-    private static class Selected extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm.getDataSet().isSelected(osm);
-        }
-
-        @Override
-        public String toString() {
-            return "selected";
-        }
-    }
-
-    /**
-     * Match objects that are incomplete, where only id and type are known.
-     * Typically some members of a relation are incomplete until they are
-     * fetched from the server.
-     */
-    private static class Incomplete extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm.isIncomplete() || (osm instanceof Relation && ((Relation) osm).hasIncompleteMembers());
-        }
-
-        @Override
-        public String toString() {
-            return "incomplete";
-        }
-    }
-
-    /**
-     * Matches objects that don't have any interesting tags (i.e. only has source,
-     * FIXME, etc.). The complete list of uninteresting tags can be found here:
-     * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
-     */
-    private static class Untagged extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return !osm.isTagged() && !osm.isIncomplete();
-        }
-
-        @Override
-        public String toString() {
-            return "untagged";
-        }
-    }
-
-    /**
-     * Matches ways which are closed (i.e. first and last node are the same)
-     */
-    private static class Closed extends Match {
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            return osm instanceof Way && ((Way) osm).isClosed();
-        }
-
-        @Override
-        public String toString() {
-            return "closed";
-        }
-    }
-
-    /**
-     * Matches objects if they are parents of the expression
-     */
-    public static class Parent extends UnaryMatch {
-        public Parent(Match m) {
-            super(m);
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            boolean isParent = false;
-
-            if (osm instanceof Way) {
-                for (Node n : ((Way) osm).getNodes()) {
-                    isParent |= match.match(n);
-                }
-            } else if (osm instanceof Relation) {
-                for (RelationMember member : ((Relation) osm).getMembers()) {
-                    isParent |= match.match(member.getMember());
-                }
-            }
-            return isParent;
-        }
-
-        @Override
-        public String toString() {
-            return "parent(" + match + ')';
-        }
-    }
-
-    /**
-     * Matches objects if they are children of the expression
-     */
-    public static class Child extends UnaryMatch {
-
-        public Child(Match m) {
-            super(m);
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            boolean isChild = false;
-            for (OsmPrimitive p : osm.getReferrers()) {
-                isChild |= match.match(p);
-            }
-            return isChild;
-        }
-
-        @Override
-        public String toString() {
-            return "child(" + match + ')';
-        }
-    }
-
-    /**
-     * Matches if the size of the area is within the given range
-     *
-     * @author Ole Jørgen Brønner
-     */
-    private static class AreaSize extends RangeMatch {
-
-        AreaSize(Range range) {
-            super(range);
-        }
-
-        AreaSize(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of numbers expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            final Double area = Geometry.computeArea(osm);
-            return area == null ? null : area.longValue();
-        }
-
-        @Override
-        protected String getString() {
-            return "areasize";
-        }
-    }
-
-    /**
-     * Matches if the length of a way is within the given range
-     */
-    private static class WayLength extends RangeMatch {
-
-        WayLength(Range range) {
-            super(range);
-        }
-
-        WayLength(PushbackTokenizer tokenizer) throws ParseError {
-            this(tokenizer.readRange(tr("Range of numbers expected")));
-        }
-
-        @Override
-        protected Long getNumber(OsmPrimitive osm) {
-            if (!(osm instanceof Way))
-                return null;
-            Way way = (Way) osm;
-            return (long) way.getLength();
-        }
-
-        @Override
-        protected String getString() {
-            return "waylength";
-        }
-    }
-
-    /**
-     * Matches objects within the given bounds.
-     */
-    private abstract static class InArea extends Match {
-
-        protected final boolean all;
-
-        /**
-         * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices.
-         */
-        InArea(boolean all) {
-            this.all = all;
-        }
-
-        protected abstract Collection<Bounds> getBounds(OsmPrimitive primitive);
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            if (!osm.isUsable())
-                return false;
-            else if (osm instanceof Node) {
-                LatLon coordinate = ((Node) osm).getCoor();
-                Collection<Bounds> allBounds = getBounds(osm);
-                return coordinate != null && allBounds != null && allBounds.stream().anyMatch(bounds -> bounds.contains(coordinate));
-            } else if (osm instanceof Way) {
-                Collection<Node> nodes = ((Way) osm).getNodes();
-                return all ? nodes.stream().allMatch(this) : nodes.stream().anyMatch(this);
-            } else if (osm instanceof Relation) {
-                Collection<OsmPrimitive> primitives = ((Relation) osm).getMemberPrimitivesList();
-                return all ? primitives.stream().allMatch(this) : primitives.stream().anyMatch(this);
-            } else
-                return false;
-        }
-    }
-
-    /**
-     * Matches objects within source area ("downloaded area").
-     */
-    public static class InDataSourceArea extends InArea {
-
-        /**
-         * Constructs a new {@code InDataSourceArea}.
-         * @param all if true, all way nodes or relation members have to be within source area; if false, one suffices.
-         */
-        public InDataSourceArea(boolean all) {
-            super(all);
-        }
-
-        @Override
-        protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
-            return primitive.getDataSet() != null ? primitive.getDataSet().getDataSourceBounds() : null;
-        }
-
-        @Override
-        public String toString() {
-            return all ? "allindownloadedarea" : "indownloadedarea";
-        }
-    }
-
-    /**
-     * Matches objects which are not outside the source area ("downloaded area").
-     * Unlike {@link InDataSourceArea} this matches also if no source area is set (e.g., for new layers).
-     */
-    public static class NotOutsideDataSourceArea extends InDataSourceArea {
-
-        /**
-         * Constructs a new {@code NotOutsideDataSourceArea}.
-         */
-        public NotOutsideDataSourceArea() {
-            super(false);
-        }
-
-        @Override
-        protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
-            final Collection<Bounds> bounds = super.getBounds(primitive);
-            return bounds == null || bounds.isEmpty() ? Collections.singleton(Main.getProjection().getWorldBoundsLatLon()) : bounds;
-        }
-
-        @Override
-        public String toString() {
-            return "NotOutsideDataSourceArea";
-        }
-    }
-
-    /**
-     * Matches objects within current map view.
-     */
-    private static class InView extends InArea {
-
-        InView(boolean all) {
-            super(all);
-        }
-
-        @Override
-        protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
-            if (!MainApplication.isDisplayingMapView()) {
-                return null;
-            }
-            return Collections.singleton(MainApplication.getMap().mapView.getRealBounds());
-        }
-
-        @Override
-        public String toString() {
-            return all ? "allinview" : "inview";
-        }
-    }
-
-    /**
-     * Matches presets.
-     * @since 12464
-     */
-    private static class Preset extends Match {
-        private final List<TaggingPreset> presets;
-
-        Preset(String presetName) throws ParseError {
-
-            if (presetName == null || presetName.isEmpty()) {
-                throw new ParseError("The name of the preset is required");
-            }
-
-            int wildCardIdx = presetName.lastIndexOf('*');
-            int length = presetName.length() - 1;
-
-            /*
-             * Match strictly (simply comparing the names) if there is no '*' symbol
-             * at the end of the name or '*' is a part of the preset name.
-             */
-            boolean matchStrictly = wildCardIdx == -1 || wildCardIdx != length;
-
-            this.presets = TaggingPresets.getTaggingPresets()
-                    .stream()
-                    .filter(preset -> !(preset instanceof TaggingPresetMenu || preset instanceof TaggingPresetSeparator))
-                    .filter(preset -> presetNameMatch(presetName, preset, matchStrictly))
-                    .collect(Collectors.toList());
-
-            if (this.presets.isEmpty()) {
-                throw new ParseError(tr("Unknown preset name: ") + presetName);
-            }
-        }
-
-        @Override
-        public boolean match(OsmPrimitive osm) {
-            for (TaggingPreset preset : this.presets) {
-                if (preset.test(osm)) {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        private static boolean presetNameMatch(String name, TaggingPreset preset, boolean matchStrictly) {
-            if (matchStrictly) {
-                return name.equalsIgnoreCase(preset.getRawName());
-            }
-
-            try {
-                String groupSuffix = name.substring(0, name.length() - 2); // try to remove '/*'
-                TaggingPresetMenu group = preset.group;
-
-                return group != null && groupSuffix.equalsIgnoreCase(group.getRawName());
-            } catch (StringIndexOutOfBoundsException ex) {
-                return false;
-            }
-        }
-    }
-
-    public static class ParseError extends Exception {
-        public ParseError(String msg) {
-            super(msg);
-        }
-
-        public ParseError(String msg, Throwable cause) {
-            super(msg, cause);
-        }
-
-        public ParseError(Token expected, Token found) {
-            this(tr("Unexpected token. Expected {0}, found {1}", expected, found));
-        }
-    }
-
-    /**
-     * Compiles the search expression.
-     * @param searchStr the search expression
-     * @return a {@link Match} object for the expression
-     * @throws ParseError if an error has been encountered while compiling
-     * @see #compile(org.openstreetmap.josm.actions.search.SearchAction.SearchSetting)
-     */
-    public static Match compile(String searchStr) throws ParseError {
-        return new SearchCompiler(false, false,
-                new PushbackTokenizer(
-                        new PushbackReader(new StringReader(searchStr))))
-                .parse();
-    }
-
-    /**
-     * Compiles the search expression.
-     * @param setting the settings to use
-     * @return a {@link Match} object for the expression
-     * @throws ParseError if an error has been encountered while compiling
-     * @see #compile(String)
-     */
-    public static Match compile(SearchAction.SearchSetting setting) throws ParseError {
-        if (setting.mapCSSSearch) {
-            return compileMapCSS(setting.text);
-        }
-        return new SearchCompiler(setting.caseSensitive, setting.regexSearch,
-                new PushbackTokenizer(
-                        new PushbackReader(new StringReader(setting.text))))
-                .parse();
-    }
-
-    static Match compileMapCSS(String mapCSS) throws ParseError {
-        try {
-            final List<Selector> selectors = new MapCSSParser(new StringReader(mapCSS)).selectors();
-            return new Match() {
-                @Override
-                public boolean match(OsmPrimitive osm) {
-                    for (Selector selector : selectors) {
-                        if (selector.matches(new Environment(osm))) {
-                            return true;
-                        }
-                    }
-                    return false;
-                }
-            };
-        } catch (ParseException e) {
-            throw new ParseError(tr("Failed to parse MapCSS selector"), e);
-        }
-    }
-
-    /**
-     * Parse search string.
-     *
-     * @return match determined by search string
-     * @throws org.openstreetmap.josm.actions.search.SearchCompiler.ParseError if search expression cannot be parsed
-     */
-    public Match parse() throws ParseError {
-        Match m = Optional.ofNullable(parseExpression()).orElse(Always.INSTANCE);
-        if (!tokenizer.readIfEqual(Token.EOF))
-            throw new ParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
-        Logging.debug("Parsed search expression is {0}", m);
-        return m;
-    }
-
-    /**
-     * Parse expression.
-     *
-     * @return match determined by parsing expression
-     * @throws ParseError if search expression cannot be parsed
-     */
-    private Match parseExpression() throws ParseError {
-        // Step 1: parse the whole expression and build a list of factors and logical tokens
-        List<Object> list = parseExpressionStep1();
-        // Step 2: iterate the list in reverse order to build the logical expression
-        // This iterative approach avoids StackOverflowError for long expressions (see #14217)
-        return parseExpressionStep2(list);
-    }
-
-    private List<Object> parseExpressionStep1() throws ParseError {
-        Match factor;
-        String token = null;
-        String errorMessage = null;
-        List<Object> list = new ArrayList<>();
-        do {
-            factor = parseFactor();
-            if (factor != null) {
-                if (token != null) {
-                    list.add(token);
-                }
-                list.add(factor);
-                if (tokenizer.readIfEqual(Token.OR)) {
-                    token = "OR";
-                    errorMessage = tr("Missing parameter for OR");
-                } else if (tokenizer.readIfEqual(Token.XOR)) {
-                    token = "XOR";
-                    errorMessage = tr("Missing parameter for XOR");
-                } else {
-                    token = "AND";
-                    errorMessage = null;
-                }
-            } else if (errorMessage != null) {
-                throw new ParseError(errorMessage);
-            }
-        } while (factor != null);
-        return list;
-    }
-
-    private static Match parseExpressionStep2(List<Object> list) {
-        Match result = null;
-        for (int i = list.size() - 1; i >= 0; i--) {
-            Object o = list.get(i);
-            if (o instanceof Match && result == null) {
-                result = (Match) o;
-            } else if (o instanceof String && i > 0) {
-                Match factor = (Match) list.get(i-1);
-                switch ((String) o) {
-                case "OR":
-                    result = new Or(factor, result);
-                    break;
-                case "XOR":
-                    result = new Xor(factor, result);
-                    break;
-                case "AND":
-                    result = new And(factor, result);
-                    break;
-                default: throw new IllegalStateException(tr("Unexpected token: {0}", o));
-                }
-                i--;
-            } else {
-                throw new IllegalStateException("i=" + i + "; o=" + o);
-            }
-        }
-        return result;
-    }
-
-    /**
-     * Parse next factor (a search operator or search term).
-     *
-     * @return match determined by parsing factor string
-     * @throws ParseError if search expression cannot be parsed
-     */
-    private Match parseFactor() throws ParseError {
-        if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
-            Match expression = parseExpression();
-            if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
-                throw new ParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
-            return expression;
-        } else if (tokenizer.readIfEqual(Token.NOT)) {
-            return new Not(parseFactor(tr("Missing operator for NOT")));
-        } else if (tokenizer.readIfEqual(Token.KEY)) {
-            // factor consists of key:value or key=value
-            String key = tokenizer.getText();
-            if (tokenizer.readIfEqual(Token.EQUALS)) {
-                return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
-            } else if (tokenizer.readIfEqual(Token.LESS_THAN)) {
-                return new ValueComparison(key, tokenizer.readTextOrNumber(), -1);
-            } else if (tokenizer.readIfEqual(Token.GREATER_THAN)) {
-                return new ValueComparison(key, tokenizer.readTextOrNumber(), +1);
-            } else if (tokenizer.readIfEqual(Token.COLON)) {
-                // see if we have a Match that takes a data parameter
-                SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
-                if (factory != null)
-                    return factory.get(key, tokenizer);
-
-                UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
-                if (unaryFactory != null)
-                    return unaryFactory.get(key, parseFactor(), tokenizer);
-
-                // key:value form where value is a string (may be OSM key search)
-                final String value = tokenizer.readTextOrNumber();
-                return new KeyValue(key, value != null ? value : "", regexSearch, caseSensitive);
-            } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
-                return new BooleanMatch(key, false);
-            else {
-                SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
-                if (factory != null)
-                    return factory.get(key, null);
-
-                UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
-                if (unaryFactory != null)
-                    return unaryFactory.get(key, parseFactor(), null);
-
-                // match string in any key or value
-                return new Any(key, regexSearch, caseSensitive);
-            }
-        } else
-            return null;
-    }
-
-    private Match parseFactor(String errorMessage) throws ParseError {
-        return Optional.ofNullable(parseFactor()).orElseThrow(() -> new ParseError(errorMessage));
-    }
-
-    private static int regexFlags(boolean caseSensitive) {
-        int searchFlags = 0;
-
-        // Enables canonical Unicode equivalence so that e.g. the two
-        // forms of "\u00e9gal" and "e\u0301gal" will match.
-        //
-        // It makes sense to match no matter how the character
-        // happened to be constructed.
-        searchFlags |= Pattern.CANON_EQ;
-
-        // Make "." match any character including newline (/s in Perl)
-        searchFlags |= Pattern.DOTALL;
-
-        // CASE_INSENSITIVE by itself only matches US-ASCII case
-        // insensitively, but the OSM data is in Unicode. With
-        // UNICODE_CASE casefolding is made Unicode-aware.
-        if (!caseSensitive) {
-            searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
-        }
-
-        return searchFlags;
-    }
-
-    static String escapeStringForSearch(String s) {
-        return s.replace("\\", "\\\\").replace("\"", "\\\"");
-    }
-
-    /**
-     * Builds a search string for the given tag. If value is empty, the existence of the key is checked.
-     *
-     * @param key   the tag key
-     * @param value the tag value
-     * @return a search string for the given tag
-     */
-    public static String buildSearchStringForTag(String key, String value) {
-        final String forKey = '"' + escapeStringForSearch(key) + '"' + '=';
-        if (value == null || value.isEmpty()) {
-            return forKey + '*';
-        } else {
-            return forKey + '"' + escapeStringForSearch(value) + '"';
-        }
-    }
-}
-
Index: /trunk/src/org/openstreetmap/josm/data/gpx/WayPoint.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/gpx/WayPoint.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/data/gpx/WayPoint.java	(revision 12656)
@@ -8,8 +8,8 @@
 import java.util.Objects;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.coor.ILatLon;
 import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.projection.Projecting;
 import org.openstreetmap.josm.tools.Logging;
Index: /trunk/src/org/openstreetmap/josm/data/osm/FilterMatcher.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/FilterMatcher.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/data/osm/FilterMatcher.java	(revision 12656)
@@ -7,8 +7,8 @@
 
 import org.openstreetmap.josm.actions.search.SearchAction.SearchMode;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Not;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Not;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.MapFrame;
@@ -79,5 +79,5 @@
         private final boolean isInverted;
 
-        FilterInfo(Filter filter) throws ParseError {
+        FilterInfo(Filter filter) throws SearchParseError {
             if (filter.mode == SearchMode.remove || filter.mode == SearchMode.in_selection) {
                 isDelete = true;
@@ -98,7 +98,7 @@
      * Clears the current filters, and adds the given filters
      * @param filters the filters to add
-     * @throws ParseError if the search expression in one of the filters cannot be parsed
-     */
-    public void update(Collection<Filter> filters) throws ParseError {
+     * @throws SearchParseError if the search expression in one of the filters cannot be parsed
+     */
+    public void update(Collection<Filter> filters) throws SearchParseError {
         reset();
         for (Filter filter : filters) {
@@ -118,7 +118,7 @@
      * Adds a filter to the currently used filters
      * @param filter the filter to add
-     * @throws ParseError if the search expression in the filter cannot be parsed
-     */
-    public void add(final Filter filter) throws ParseError {
+     * @throws SearchParseError if the search expression in the filter cannot be parsed
+     */
+    public void add(final Filter filter) throws SearchParseError {
         if (!filter.enable) {
             return;
@@ -330,8 +330,8 @@
      * @param filters filters to add to the resulting filter matcher
      * @return a new {@code FilterMatcher} containing the given filters
-     * @throws ParseError if the search expression in a filter cannot be parsed
+     * @throws SearchParseError if the search expression in a filter cannot be parsed
      * @since 12383
      */
-    public static FilterMatcher of(Filter... filters) throws ParseError {
+    public static FilterMatcher of(Filter... filters) throws SearchParseError {
         FilterMatcher result = new FilterMatcher();
         for (Filter filter : filters) {
Index: /trunk/src/org/openstreetmap/josm/data/osm/FilterModel.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/FilterModel.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/data/osm/FilterModel.java	(revision 12656)
@@ -17,6 +17,6 @@
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
@@ -52,5 +52,5 @@
             try {
                 filterMatcher.add(filter);
-            } catch (ParseError e) {
+            } catch (SearchParseError e) {
                 Logging.error(e);
                 JOptionPane.showMessageDialog(
Index: /trunk/src/org/openstreetmap/josm/data/osm/FilterWorker.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/FilterWorker.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/data/osm/FilterWorker.java	(revision 12656)
@@ -5,6 +5,6 @@
 import java.util.Collections;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.data.osm.FilterMatcher.FilterType;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.tools.SubclassFilteredCollection;
 
@@ -28,8 +28,8 @@
      * @param filters the filters
      * @return true, if the filter state (normal / disabled / hidden) of any primitive has changed in the process
-     * @throws ParseError if the search expression in a filter cannot be parsed
+     * @throws SearchParseError if the search expression in a filter cannot be parsed
      * @since 12383
      */
-    public static boolean executeFilters(Collection<OsmPrimitive> all, Filter... filters) throws ParseError {
+    public static boolean executeFilters(Collection<OsmPrimitive> all, Filter... filters) throws SearchParseError {
         return executeFilters(all, FilterMatcher.of(filters));
     }
Index: /trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/data/osm/OsmPrimitive.java	(revision 12656)
@@ -21,7 +21,7 @@
 
 import org.openstreetmap.josm.Main;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.osm.visitor.Visitor;
 import org.openstreetmap.josm.gui.mappaint.StyleCache;
@@ -761,5 +761,5 @@
         try {
             return SearchCompiler.compile(Main.pref.get(prefName, defaultValue));
-        } catch (ParseError e) {
+        } catch (SearchParseError e) {
             Logging.log(Logging.LEVEL_ERROR, "Unable to compile pattern for " + prefName + ", trying default pattern:", e);
         }
@@ -767,5 +767,5 @@
         try {
             return SearchCompiler.compile(defaultValue);
-        } catch (ParseError e2) {
+        } catch (SearchParseError e2) {
             throw new AssertionError("Unable to compile default pattern for direction keys: " + e2.getMessage(), e2);
         }
Index: /trunk/src/org/openstreetmap/josm/data/osm/search/PushbackTokenizer.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/search/PushbackTokenizer.java	(revision 12656)
+++ /trunk/src/org/openstreetmap/josm/data/osm/search/PushbackTokenizer.java	(revision 12656)
@@ -0,0 +1,350 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.search;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.IOException;
+import java.io.Reader;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Objects;
+
+import org.openstreetmap.josm.tools.JosmRuntimeException;
+
+/**
+ * This class is used to parse a search string and split it into tokens.
+ * It provides methods to parse numbers and extract strings.
+ * @since 12656 (moved from actions.search package)
+ */
+public class PushbackTokenizer {
+
+    /**
+     * A range of long numbers. Immutable
+     */
+    public static class Range {
+        private final long start;
+        private final long end;
+
+        /**
+         * Create a new range
+         * @param start The start
+         * @param end The end (inclusive)
+         */
+        public Range(long start, long end) {
+            this.start = start;
+            this.end = end;
+        }
+
+        /**
+         * @return The start
+         */
+        public long getStart() {
+            return start;
+        }
+
+        /**
+         * @return The end (inclusive)
+         */
+        public long getEnd() {
+            return end;
+        }
+
+        @Override
+        public String toString() {
+            return "Range [start=" + start + ", end=" + end + ']';
+        }
+    }
+
+    private final Reader search;
+
+    private Token currentToken;
+    private String currentText;
+    private Long currentNumber;
+    private Long currentRange;
+    private int c;
+    private boolean isRange;
+
+    /**
+     * Creates a new {@link PushbackTokenizer}
+     * @param search The search string reader to read the tokens from
+     */
+    public PushbackTokenizer(Reader search) {
+        this.search = search;
+        getChar();
+    }
+
+    /**
+     * The token types that may be read
+     */
+    public enum Token {
+        /**
+         * Not token (-)
+         */
+        NOT(marktr("<not>")),
+        /**
+         * Or token (or) (|)
+         */
+        OR(marktr("<or>")),
+        /**
+         * Xor token (xor) (^)
+         */
+        XOR(marktr("<xor>")),
+        /**
+         * opening parentheses token (
+         */
+        LEFT_PARENT(marktr("<left parent>")),
+        /**
+         * closing parentheses token )
+         */
+        RIGHT_PARENT(marktr("<right parent>")),
+        /**
+         * Colon :
+         */
+        COLON(marktr("<colon>")),
+        /**
+         * The equals sign (=)
+         */
+        EQUALS(marktr("<equals>")),
+        /**
+         * A text
+         */
+        KEY(marktr("<key>")),
+        /**
+         * A question mark (?)
+         */
+        QUESTION_MARK(marktr("<question mark>")),
+        /**
+         * Marks the end of the input
+         */
+        EOF(marktr("<end-of-file>")),
+        /**
+         * Less than sign (&lt;)
+         */
+        LESS_THAN("<less-than>"),
+        /**
+         * Greater than sign (&gt;)
+         */
+        GREATER_THAN("<greater-than>");
+
+        Token(String name) {
+            this.name = name;
+        }
+
+        private final String name;
+
+        @Override
+        public String toString() {
+            return tr(name);
+        }
+    }
+
+    private void getChar() {
+        try {
+            c = search.read();
+        } catch (IOException e) {
+            throw new JosmRuntimeException(e.getMessage(), e);
+        }
+    }
+
+    private static final List<Character> SPECIAL_CHARS = Arrays.asList('"', ':', '(', ')', '|', '^', '=', '?', '<', '>');
+    private static final List<Character> SPECIAL_CHARS_QUOTED = Arrays.asList('"');
+
+    private String getString(boolean quoted) {
+        List<Character> sChars = quoted ? SPECIAL_CHARS_QUOTED : SPECIAL_CHARS;
+        StringBuilder s = new StringBuilder();
+        boolean escape = false;
+        while (c != -1 && (escape || (!sChars.contains((char) c) && (quoted || !Character.isWhitespace(c))))) {
+            if (c == '\\' && !escape) {
+                escape = true;
+            } else {
+                s.append((char) c);
+                escape = false;
+            }
+            getChar();
+        }
+        return s.toString();
+    }
+
+    private String getString() {
+        return getString(false);
+    }
+
+    /**
+     * The token returned is <code>null</code> or starts with an identifier character:
+     * - for an '-'. This will be the only character
+     * : for an key. The value is the next token
+     * | for "OR"
+     * ^ for "XOR"
+     * ' ' for anything else.
+     * @return The next token in the stream.
+     */
+    public Token nextToken() {
+        if (currentToken != null) {
+            Token result = currentToken;
+            currentToken = null;
+            return result;
+        }
+
+        while (Character.isWhitespace(c)) {
+            getChar();
+        }
+        switch (c) {
+        case -1:
+            getChar();
+            return Token.EOF;
+        case ':':
+            getChar();
+            return Token.COLON;
+        case '=':
+            getChar();
+            return Token.EQUALS;
+        case '<':
+            getChar();
+            return Token.LESS_THAN;
+        case '>':
+            getChar();
+            return Token.GREATER_THAN;
+        case '(':
+            getChar();
+            return Token.LEFT_PARENT;
+        case ')':
+            getChar();
+            return Token.RIGHT_PARENT;
+        case '|':
+            getChar();
+            return Token.OR;
+        case '^':
+            getChar();
+            return Token.XOR;
+        case '&':
+            getChar();
+            return nextToken();
+        case '?':
+            getChar();
+            return Token.QUESTION_MARK;
+        case '"':
+            getChar();
+            currentText = getString(true);
+            getChar();
+            return Token.KEY;
+        default:
+            String prefix = "";
+            if (c == '-') {
+                getChar();
+                if (!Character.isDigit(c))
+                    return Token.NOT;
+                prefix = "-";
+            }
+            currentText = prefix + getString();
+            if ("or".equalsIgnoreCase(currentText))
+                return Token.OR;
+            else if ("xor".equalsIgnoreCase(currentText))
+                return Token.XOR;
+            else if ("and".equalsIgnoreCase(currentText))
+                return nextToken();
+            // try parsing number
+            try {
+                currentNumber = Long.valueOf(currentText);
+            } catch (NumberFormatException e) {
+                currentNumber = null;
+            }
+            // if text contains "-", try parsing a range
+            int pos = currentText.indexOf('-', 1);
+            isRange = pos > 0;
+            if (isRange) {
+                try {
+                    currentNumber = Long.valueOf(currentText.substring(0, pos));
+                } catch (NumberFormatException e) {
+                    currentNumber = null;
+                }
+                try {
+                    currentRange = Long.valueOf(currentText.substring(pos + 1));
+                } catch (NumberFormatException e) {
+                    currentRange = null;
+                    }
+                } else {
+                    currentRange = null;
+                }
+            return Token.KEY;
+        }
+    }
+
+    /**
+     * Reads the next token if it is equal to the given, suggested token
+     * @param token The token the next one should be equal to
+     * @return <code>true</code> if it has been read
+     */
+    public boolean readIfEqual(Token token) {
+        Token nextTok = nextToken();
+        if (Objects.equals(nextTok, token))
+            return true;
+        currentToken = nextTok;
+        return false;
+    }
+
+    /**
+     * Reads the next token. If it is a text, return that text. If not, advance
+     * @return the text or <code>null</code> if the reader was advanced
+     */
+    public String readTextOrNumber() {
+        Token nextTok = nextToken();
+        if (nextTok == Token.KEY)
+            return currentText;
+        currentToken = nextTok;
+        return null;
+    }
+
+    /**
+     * Reads a number
+     * @param errorMessage The error if the number cannot be read
+     * @return The number that was found
+     * @throws SearchParseError if there is no number
+     */
+    public long readNumber(String errorMessage) throws SearchParseError {
+        if ((nextToken() == Token.KEY) && (currentNumber != null))
+            return currentNumber;
+        else
+            throw new SearchParseError(errorMessage);
+    }
+
+    /**
+     * Gets the last number that was read
+     * @return The last number
+     */
+    public long getReadNumber() {
+        return (currentNumber != null) ? currentNumber : 0;
+    }
+
+    /**
+     * Reads a range of numbers
+     * @param errorMessage The error if the input is malformed
+     * @return The range that was found
+     * @throws SearchParseError If the input is not as expected for a range
+     */
+    public Range readRange(String errorMessage) throws SearchParseError {
+        if (nextToken() != Token.KEY || (currentNumber == null && currentRange == null)) {
+            throw new SearchParseError(errorMessage);
+        } else if (!isRange && currentNumber != null) {
+            if (currentNumber >= 0) {
+                return new Range(currentNumber, currentNumber);
+            } else {
+                return new Range(0, Math.abs(currentNumber));
+            }
+        } else if (isRange && currentRange == null) {
+            return new Range(currentNumber, Long.MAX_VALUE);
+        } else if (currentNumber != null && currentRange != null) {
+            return new Range(currentNumber, currentRange);
+        } else {
+            throw new SearchParseError(errorMessage);
+        }
+    }
+
+    /**
+     * Gets the last text that was found
+     * @return The text
+     */
+    public String getText() {
+        return currentText;
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java	(revision 12656)
+++ /trunk/src/org/openstreetmap/josm/data/osm/search/SearchCompiler.java	(revision 12656)
@@ -0,0 +1,1863 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.search;
+
+import static org.openstreetmap.josm.tools.I18n.marktr;
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.io.PushbackReader;
+import java.io.StringReader;
+import java.text.Normalizer;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Optional;
+import java.util.function.Predicate;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.regex.PatternSyntaxException;
+import java.util.stream.Collectors;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.actions.search.SearchAction;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.OsmUtils;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Tagged;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.PushbackTokenizer.Range;
+import org.openstreetmap.josm.data.osm.search.PushbackTokenizer.Token;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.mappaint.Environment;
+import org.openstreetmap.josm.gui.mappaint.mapcss.Selector;
+import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.MapCSSParser;
+import org.openstreetmap.josm.gui.mappaint.mapcss.parsergen.ParseException;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetSeparator;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
+import org.openstreetmap.josm.tools.AlphanumComparator;
+import org.openstreetmap.josm.tools.Geometry;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.UncheckedParseException;
+import org.openstreetmap.josm.tools.Utils;
+import org.openstreetmap.josm.tools.date.DateUtils;
+
+/**
+ * Implements a google-like search.
+ * <br>
+ * Grammar:
+ * <pre>
+ * expression =
+ *   fact | expression
+ *   fact expression
+ *   fact
+ * 
+ * fact =
+ *  ( expression )
+ *  -fact
+ *  term?
+ *  term=term
+ *  term:term
+ *  term
+ *  </pre>
+ * 
+ * @author Imi
+ * @since 12656 (moved from actions.search package)
+ */
+public class SearchCompiler {
+
+    private final boolean caseSensitive;
+    private final boolean regexSearch;
+    private static String rxErrorMsg = marktr("The regex \"{0}\" had a parse error at offset {1}, full error:\n\n{2}");
+    private static String rxErrorMsgNoPos = marktr("The regex \"{0}\" had a parse error, full error:\n\n{1}");
+    private final PushbackTokenizer tokenizer;
+    private static Map<String, SimpleMatchFactory> simpleMatchFactoryMap = new HashMap<>();
+    private static Map<String, UnaryMatchFactory> unaryMatchFactoryMap = new HashMap<>();
+    private static Map<String, BinaryMatchFactory> binaryMatchFactoryMap = new HashMap<>();
+
+    public SearchCompiler(boolean caseSensitive, boolean regexSearch, PushbackTokenizer tokenizer) {
+        this.caseSensitive = caseSensitive;
+        this.regexSearch = regexSearch;
+        this.tokenizer = tokenizer;
+
+        // register core match factories at first instance, so plugins should never be able to generate a NPE
+        if (simpleMatchFactoryMap.isEmpty()) {
+            addMatchFactory(new CoreSimpleMatchFactory());
+        }
+        if (unaryMatchFactoryMap.isEmpty()) {
+            addMatchFactory(new CoreUnaryMatchFactory());
+        }
+    }
+
+    /**
+     * Add (register) MatchFactory with SearchCompiler
+     * @param factory match factory
+     */
+    public static void addMatchFactory(MatchFactory factory) {
+        for (String keyword : factory.getKeywords()) {
+            final MatchFactory existing;
+            if (factory instanceof SimpleMatchFactory) {
+                existing = simpleMatchFactoryMap.put(keyword, (SimpleMatchFactory) factory);
+            } else if (factory instanceof UnaryMatchFactory) {
+                existing = unaryMatchFactoryMap.put(keyword, (UnaryMatchFactory) factory);
+            } else if (factory instanceof BinaryMatchFactory) {
+                existing = binaryMatchFactoryMap.put(keyword, (BinaryMatchFactory) factory);
+            } else
+                throw new AssertionError("Unknown match factory");
+            if (existing != null) {
+                Logging.warn("SearchCompiler: for key ''{0}'', overriding match factory ''{1}'' with ''{2}''", keyword, existing, factory);
+            }
+        }
+    }
+
+    public class CoreSimpleMatchFactory implements SimpleMatchFactory {
+        private final Collection<String> keywords = Arrays.asList("id", "version", "type", "user", "role",
+                "changeset", "nodes", "ways", "tags", "areasize", "waylength", "modified", "deleted", "selected",
+                "incomplete", "untagged", "closed", "new", "indownloadedarea",
+                "allindownloadedarea", "inview", "allinview", "timestamp", "nth", "nth%", "hasRole", "preset");
+
+        @Override
+        public Match get(String keyword, PushbackTokenizer tokenizer) throws SearchParseError {
+            switch(keyword) {
+            case "modified":
+                return new Modified();
+            case "deleted":
+                return new Deleted();
+            case "selected":
+                return new Selected();
+            case "incomplete":
+                return new Incomplete();
+            case "untagged":
+                return new Untagged();
+            case "closed":
+                return new Closed();
+            case "new":
+                return new New();
+            case "indownloadedarea":
+                return new InDataSourceArea(false);
+            case "allindownloadedarea":
+                return new InDataSourceArea(true);
+            case "inview":
+                return new InView(false);
+            case "allinview":
+                return new InView(true);
+            default:
+                if (tokenizer != null) {
+                    switch (keyword) {
+                    case "id":
+                        return new Id(tokenizer);
+                    case "version":
+                        return new Version(tokenizer);
+                    case "type":
+                        return new ExactType(tokenizer.readTextOrNumber());
+                    case "preset":
+                        return new Preset(tokenizer.readTextOrNumber());
+                    case "user":
+                        return new UserMatch(tokenizer.readTextOrNumber());
+                    case "role":
+                        return new RoleMatch(tokenizer.readTextOrNumber());
+                    case "changeset":
+                        return new ChangesetId(tokenizer);
+                    case "nodes":
+                        return new NodeCountRange(tokenizer);
+                    case "ways":
+                        return new WayCountRange(tokenizer);
+                    case "tags":
+                        return new TagCountRange(tokenizer);
+                    case "areasize":
+                        return new AreaSize(tokenizer);
+                    case "waylength":
+                        return new WayLength(tokenizer);
+                    case "nth":
+                        return new Nth(tokenizer, false);
+                    case "nth%":
+                        return new Nth(tokenizer, true);
+                    case "hasRole":
+                        return new HasRole(tokenizer);
+                    case "timestamp":
+                        // add leading/trailing space in order to get expected split (e.g. "a--" => {"a", ""})
+                        String rangeS = ' ' + tokenizer.readTextOrNumber() + ' ';
+                        String[] rangeA = rangeS.split("/");
+                        if (rangeA.length == 1) {
+                            return new KeyValue(keyword, rangeS.trim(), regexSearch, caseSensitive);
+                        } else if (rangeA.length == 2) {
+                            String rangeA1 = rangeA[0].trim();
+                            String rangeA2 = rangeA[1].trim();
+                            final long minDate;
+                            final long maxDate;
+                            try {
+                                // if min timestap is empty: use lowest possible date
+                                minDate = DateUtils.fromString(rangeA1.isEmpty() ? "1980" : rangeA1).getTime();
+                            } catch (UncheckedParseException ex) {
+                                throw new SearchParseError(tr("Cannot parse timestamp ''{0}''", rangeA1), ex);
+                            }
+                            try {
+                                // if max timestamp is empty: use "now"
+                                maxDate = rangeA2.isEmpty() ? System.currentTimeMillis() : DateUtils.fromString(rangeA2).getTime();
+                            } catch (UncheckedParseException ex) {
+                                throw new SearchParseError(tr("Cannot parse timestamp ''{0}''", rangeA2), ex);
+                            }
+                            return new TimestampRange(minDate, maxDate);
+                        } else {
+                            throw new SearchParseError("<html>" + tr("Expecting {0} after {1}", "<i>min</i>/<i>max</i>", "<i>timestamp</i>"));
+                        }
+                    }
+                } else {
+                    throw new SearchParseError("<html>" + tr("Expecting {0} after {1}", "<code>:</code>", "<i>" + keyword + "</i>"));
+                }
+            }
+            throw new IllegalStateException("Not expecting keyword " + keyword);
+        }
+
+        @Override
+        public Collection<String> getKeywords() {
+            return keywords;
+        }
+    }
+
+    public static class CoreUnaryMatchFactory implements UnaryMatchFactory {
+        private static Collection<String> keywords = Arrays.asList("parent", "child");
+
+        @Override
+        public UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) {
+            if ("parent".equals(keyword))
+                return new Parent(matchOperand);
+            else if ("child".equals(keyword))
+                return new Child(matchOperand);
+            return null;
+        }
+
+        @Override
+        public Collection<String> getKeywords() {
+            return keywords;
+        }
+    }
+
+    /**
+     * Classes implementing this interface can provide Match operators.
+     * @since 10600 (functional interface)
+     */
+    @FunctionalInterface
+    private interface MatchFactory {
+        Collection<String> getKeywords();
+    }
+
+    public interface SimpleMatchFactory extends MatchFactory {
+        Match get(String keyword, PushbackTokenizer tokenizer) throws SearchParseError;
+    }
+
+    public interface UnaryMatchFactory extends MatchFactory {
+        UnaryMatch get(String keyword, Match matchOperand, PushbackTokenizer tokenizer) throws SearchParseError;
+    }
+
+    public interface BinaryMatchFactory extends MatchFactory {
+        AbstractBinaryMatch get(String keyword, Match lhs, Match rhs, PushbackTokenizer tokenizer) throws SearchParseError;
+    }
+
+    /**
+     * Base class for all search criteria. If the criterion only depends on an object's tags,
+     * inherit from {@link org.openstreetmap.josm.data.osm.search.SearchCompiler.TaggedMatch}.
+     */
+    public abstract static class Match implements Predicate<OsmPrimitive> {
+
+        /**
+         * Tests whether the primitive matches this criterion.
+         * @param osm the primitive to test
+         * @return true if the primitive matches this criterion
+         */
+        public abstract boolean match(OsmPrimitive osm);
+
+        /**
+         * Tests whether the tagged object matches this criterion.
+         * @param tagged the tagged object to test
+         * @return true if the tagged object matches this criterion
+         */
+        public boolean match(Tagged tagged) {
+            return false;
+        }
+
+        @Override
+        public final boolean test(OsmPrimitive object) {
+            return match(object);
+        }
+    }
+
+    public abstract static class TaggedMatch extends Match {
+
+        @Override
+        public abstract boolean match(Tagged tags);
+
+        @Override
+        public final boolean match(OsmPrimitive osm) {
+            return match((Tagged) osm);
+        }
+    }
+
+    /**
+     * A unary search operator which may take data parameters.
+     */
+    public abstract static class UnaryMatch extends Match {
+
+        protected final Match match;
+
+        public UnaryMatch(Match match) {
+            if (match == null) {
+                // "operator" (null) should mean the same as "operator()"
+                // (Always). I.e. match everything
+                this.match = Always.INSTANCE;
+            } else {
+                this.match = match;
+            }
+        }
+
+        public Match getOperand() {
+            return match;
+        }
+    }
+
+    /**
+     * A binary search operator which may take data parameters.
+     */
+    public abstract static class AbstractBinaryMatch extends Match {
+
+        protected final Match lhs;
+        protected final Match rhs;
+
+        /**
+         * Constructs a new {@code BinaryMatch}.
+         * @param lhs Left hand side
+         * @param rhs Right hand side
+         */
+        public AbstractBinaryMatch(Match lhs, Match rhs) {
+            this.lhs = lhs;
+            this.rhs = rhs;
+        }
+
+        /**
+         * Returns left hand side.
+         * @return left hand side
+         */
+        public final Match getLhs() {
+            return lhs;
+        }
+
+        /**
+         * Returns right hand side.
+         * @return right hand side
+         */
+        public final Match getRhs() {
+            return rhs;
+        }
+
+        protected static String parenthesis(Match m) {
+            return '(' + m.toString() + ')';
+        }
+    }
+
+    /**
+     * Matches every OsmPrimitive.
+     */
+    public static class Always extends TaggedMatch {
+        /** The unique instance/ */
+        public static final Always INSTANCE = new Always();
+        @Override
+        public boolean match(Tagged osm) {
+            return true;
+        }
+    }
+
+    /**
+     * Never matches any OsmPrimitive.
+     */
+    public static class Never extends TaggedMatch {
+        /** The unique instance/ */
+        public static final Never INSTANCE = new Never();
+        @Override
+        public boolean match(Tagged osm) {
+            return false;
+        }
+    }
+
+    /**
+     * Inverts the match.
+     */
+    public static class Not extends UnaryMatch {
+        public Not(Match match) {
+            super(match);
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return !match.match(osm);
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            return !match.match(osm);
+        }
+
+        @Override
+        public String toString() {
+            return '!' + match.toString();
+        }
+
+        public Match getMatch() {
+            return match;
+        }
+    }
+
+    /**
+     * Matches if the value of the corresponding key is ''yes'', ''true'', ''1'' or ''on''.
+     */
+    private static class BooleanMatch extends TaggedMatch {
+        private final String key;
+        private final boolean defaultValue;
+
+        BooleanMatch(String key, boolean defaultValue) {
+            this.key = key;
+            this.defaultValue = defaultValue;
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            return Optional.ofNullable(OsmUtils.getOsmBoolean(osm.get(key))).orElse(defaultValue);
+        }
+
+        @Override
+        public String toString() {
+            return key + '?';
+        }
+    }
+
+    /**
+     * Matches if both left and right expressions match.
+     */
+    public static class And extends AbstractBinaryMatch {
+        /**
+         * Constructs a new {@code And} match.
+         * @param lhs left hand side
+         * @param rhs right hand side
+         */
+        public And(Match lhs, Match rhs) {
+            super(lhs, rhs);
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return lhs.match(osm) && rhs.match(osm);
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            return lhs.match(osm) && rhs.match(osm);
+        }
+
+        @Override
+        public String toString() {
+            return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof And) ? parenthesis(lhs) : lhs) + " && "
+                 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof And) ? parenthesis(rhs) : rhs);
+        }
+    }
+
+    /**
+     * Matches if the left OR the right expression match.
+     */
+    public static class Or extends AbstractBinaryMatch {
+        /**
+         * Constructs a new {@code Or} match.
+         * @param lhs left hand side
+         * @param rhs right hand side
+         */
+        public Or(Match lhs, Match rhs) {
+            super(lhs, rhs);
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return lhs.match(osm) || rhs.match(osm);
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            return lhs.match(osm) || rhs.match(osm);
+        }
+
+        @Override
+        public String toString() {
+            return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof Or) ? parenthesis(lhs) : lhs) + " || "
+                 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof Or) ? parenthesis(rhs) : rhs);
+        }
+    }
+
+    /**
+     * Matches if the left OR the right expression match, but not both.
+     */
+    public static class Xor extends AbstractBinaryMatch {
+        /**
+         * Constructs a new {@code Xor} match.
+         * @param lhs left hand side
+         * @param rhs right hand side
+         */
+        public Xor(Match lhs, Match rhs) {
+            super(lhs, rhs);
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return lhs.match(osm) ^ rhs.match(osm);
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            return lhs.match(osm) ^ rhs.match(osm);
+        }
+
+        @Override
+        public String toString() {
+            return (lhs instanceof AbstractBinaryMatch && !(lhs instanceof Xor) ? parenthesis(lhs) : lhs) + " ^ "
+                 + (rhs instanceof AbstractBinaryMatch && !(rhs instanceof Xor) ? parenthesis(rhs) : rhs);
+        }
+    }
+
+    /**
+     * Matches objects with ID in the given range.
+     */
+    private static class Id extends RangeMatch {
+        Id(Range range) {
+            super(range);
+        }
+
+        Id(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of primitive ids expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            return osm.isNew() ? 0 : osm.getUniqueId();
+        }
+
+        @Override
+        protected String getString() {
+            return "id";
+        }
+    }
+
+    /**
+     * Matches objects with a changeset ID in the given range.
+     */
+    private static class ChangesetId extends RangeMatch {
+        ChangesetId(Range range) {
+            super(range);
+        }
+
+        ChangesetId(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of changeset ids expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            return (long) osm.getChangesetId();
+        }
+
+        @Override
+        protected String getString() {
+            return "changeset";
+        }
+    }
+
+    /**
+     * Matches objects with a version number in the given range.
+     */
+    private static class Version extends RangeMatch {
+        Version(Range range) {
+            super(range);
+        }
+
+        Version(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of versions expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            return (long) osm.getVersion();
+        }
+
+        @Override
+        protected String getString() {
+            return "version";
+        }
+    }
+
+    /**
+     * Matches objects with the given key-value pair.
+     */
+    private static class KeyValue extends TaggedMatch {
+        private final String key;
+        private final Pattern keyPattern;
+        private final String value;
+        private final Pattern valuePattern;
+        private final boolean caseSensitive;
+
+        KeyValue(String key, String value, boolean regexSearch, boolean caseSensitive) throws SearchParseError {
+            this.caseSensitive = caseSensitive;
+            if (regexSearch) {
+                int searchFlags = regexFlags(caseSensitive);
+
+                try {
+                    this.keyPattern = Pattern.compile(key, searchFlags);
+                } catch (PatternSyntaxException e) {
+                    throw new SearchParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
+                } catch (IllegalArgumentException e) {
+                    throw new SearchParseError(tr(rxErrorMsgNoPos, key, e.getMessage()), e);
+                }
+                try {
+                    this.valuePattern = Pattern.compile(value, searchFlags);
+                } catch (PatternSyntaxException e) {
+                    throw new SearchParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
+                } catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
+                    throw new SearchParseError(tr(rxErrorMsgNoPos, value, e.getMessage()), e);
+                }
+                this.key = key;
+                this.value = value;
+
+            } else {
+                this.key = key;
+                this.value = value;
+                this.keyPattern = null;
+                this.valuePattern = null;
+            }
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+
+            if (keyPattern != null) {
+                if (!osm.hasKeys())
+                    return false;
+
+                /* The string search will just get a key like
+                 * 'highway' and look that up as osm.get(key). But
+                 * since we're doing a regex match we'll have to loop
+                 * over all the keys to see if they match our regex,
+                 * and only then try to match against the value
+                 */
+
+                for (String k: osm.keySet()) {
+                    String v = osm.get(k);
+
+                    Matcher matcherKey = keyPattern.matcher(k);
+                    boolean matchedKey = matcherKey.find();
+
+                    if (matchedKey) {
+                        Matcher matcherValue = valuePattern.matcher(v);
+                        boolean matchedValue = matcherValue.find();
+
+                        if (matchedValue)
+                            return true;
+                    }
+                }
+            } else {
+                String mv;
+
+                if ("timestamp".equals(key) && osm instanceof OsmPrimitive) {
+                    mv = DateUtils.fromTimestamp(((OsmPrimitive) osm).getRawTimestamp());
+                } else {
+                    mv = osm.get(key);
+                    if (!caseSensitive && mv == null) {
+                        for (String k: osm.keySet()) {
+                            if (key.equalsIgnoreCase(k)) {
+                                mv = osm.get(k);
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                if (mv == null)
+                    return false;
+
+                String v1 = caseSensitive ? mv : mv.toLowerCase(Locale.ENGLISH);
+                String v2 = caseSensitive ? value : value.toLowerCase(Locale.ENGLISH);
+
+                v1 = Normalizer.normalize(v1, Normalizer.Form.NFC);
+                v2 = Normalizer.normalize(v2, Normalizer.Form.NFC);
+                return v1.indexOf(v2) != -1;
+            }
+
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return key + '=' + value;
+        }
+    }
+
+    public static class ValueComparison extends TaggedMatch {
+        private final String key;
+        private final String referenceValue;
+        private final Double referenceNumber;
+        private final int compareMode;
+        private static final Pattern ISO8601 = Pattern.compile("\\d+-\\d+-\\d+");
+
+        public ValueComparison(String key, String referenceValue, int compareMode) {
+            this.key = key;
+            this.referenceValue = referenceValue;
+            Double v = null;
+            try {
+                if (referenceValue != null) {
+                    v = Double.valueOf(referenceValue);
+                }
+            } catch (NumberFormatException ignore) {
+                Logging.trace(ignore);
+            }
+            this.referenceNumber = v;
+            this.compareMode = compareMode;
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            final String currentValue = osm.get(key);
+            final int compareResult;
+            if (currentValue == null) {
+                return false;
+            } else if (ISO8601.matcher(currentValue).matches() || ISO8601.matcher(referenceValue).matches()) {
+                compareResult = currentValue.compareTo(referenceValue);
+            } else if (referenceNumber != null) {
+                try {
+                    compareResult = Double.compare(Double.parseDouble(currentValue), referenceNumber);
+                } catch (NumberFormatException ignore) {
+                    return false;
+                }
+            } else {
+                compareResult = AlphanumComparator.getInstance().compare(currentValue, referenceValue);
+            }
+            return compareMode < 0 ? compareResult < 0 : compareMode > 0 ? compareResult > 0 : compareResult == 0;
+        }
+
+        @Override
+        public String toString() {
+            return key + (compareMode == -1 ? "<" : compareMode == +1 ? ">" : "") + referenceValue;
+        }
+    }
+
+    /**
+     * Matches objects with the exact given key-value pair.
+     */
+    public static class ExactKeyValue extends TaggedMatch {
+
+        enum Mode {
+            ANY, ANY_KEY, ANY_VALUE, EXACT, NONE, MISSING_KEY,
+            ANY_KEY_REGEXP, ANY_VALUE_REGEXP, EXACT_REGEXP, MISSING_KEY_REGEXP;
+        }
+
+        private final String key;
+        private final String value;
+        private final Pattern keyPattern;
+        private final Pattern valuePattern;
+        private final Mode mode;
+
+        /**
+         * Constructs a new {@code ExactKeyValue}.
+         * @param regexp regular expression
+         * @param key key
+         * @param value value
+         * @throws SearchParseError if a parse error occurs
+         */
+        public ExactKeyValue(boolean regexp, String key, String value) throws SearchParseError {
+            if ("".equals(key))
+                throw new SearchParseError(tr("Key cannot be empty when tag operator is used. Sample use: key=value"));
+            this.key = key;
+            this.value = value == null ? "" : value;
+            if ("".equals(this.value) && "*".equals(key)) {
+                mode = Mode.NONE;
+            } else if ("".equals(this.value)) {
+                if (regexp) {
+                    mode = Mode.MISSING_KEY_REGEXP;
+                } else {
+                    mode = Mode.MISSING_KEY;
+                }
+            } else if ("*".equals(key) && "*".equals(this.value)) {
+                mode = Mode.ANY;
+            } else if ("*".equals(key)) {
+                if (regexp) {
+                    mode = Mode.ANY_KEY_REGEXP;
+                } else {
+                    mode = Mode.ANY_KEY;
+                }
+            } else if ("*".equals(this.value)) {
+                if (regexp) {
+                    mode = Mode.ANY_VALUE_REGEXP;
+                } else {
+                    mode = Mode.ANY_VALUE;
+                }
+            } else {
+                if (regexp) {
+                    mode = Mode.EXACT_REGEXP;
+                } else {
+                    mode = Mode.EXACT;
+                }
+            }
+
+            if (regexp && !key.isEmpty() && !"*".equals(key)) {
+                try {
+                    keyPattern = Pattern.compile(key, regexFlags(false));
+                } catch (PatternSyntaxException e) {
+                    throw new SearchParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
+                } catch (IllegalArgumentException e) {
+                    throw new SearchParseError(tr(rxErrorMsgNoPos, key, e.getMessage()), e);
+                }
+            } else {
+                keyPattern = null;
+            }
+            if (regexp && !this.value.isEmpty() && !"*".equals(this.value)) {
+                try {
+                    valuePattern = Pattern.compile(this.value, regexFlags(false));
+                } catch (PatternSyntaxException e) {
+                    throw new SearchParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
+                } catch (IllegalArgumentException e) {
+                    throw new SearchParseError(tr(rxErrorMsgNoPos, value, e.getMessage()), e);
+                }
+            } else {
+                valuePattern = null;
+            }
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+
+            if (!osm.hasKeys())
+                return mode == Mode.NONE;
+
+            switch (mode) {
+            case NONE:
+                return false;
+            case MISSING_KEY:
+                return osm.get(key) == null;
+            case ANY:
+                return true;
+            case ANY_VALUE:
+                return osm.get(key) != null;
+            case ANY_KEY:
+                for (String v:osm.getKeys().values()) {
+                    if (v.equals(value))
+                        return true;
+                }
+                return false;
+            case EXACT:
+                return value.equals(osm.get(key));
+            case ANY_KEY_REGEXP:
+                for (String v:osm.getKeys().values()) {
+                    if (valuePattern.matcher(v).matches())
+                        return true;
+                }
+                return false;
+            case ANY_VALUE_REGEXP:
+            case EXACT_REGEXP:
+                for (String k : osm.keySet()) {
+                    if (keyPattern.matcher(k).matches()
+                            && (mode == Mode.ANY_VALUE_REGEXP || valuePattern.matcher(osm.get(k)).matches()))
+                        return true;
+                }
+                return false;
+            case MISSING_KEY_REGEXP:
+                for (String k:osm.keySet()) {
+                    if (keyPattern.matcher(k).matches())
+                        return false;
+                }
+                return true;
+            }
+            throw new AssertionError("Missed state");
+        }
+
+        @Override
+        public String toString() {
+            return key + '=' + value;
+        }
+    }
+
+    /**
+     * Match a string in any tags (key or value), with optional regex and case insensitivity.
+     */
+    private static class Any extends TaggedMatch {
+        private final String search;
+        private final Pattern searchRegex;
+        private final boolean caseSensitive;
+
+        Any(String s, boolean regexSearch, boolean caseSensitive) throws SearchParseError {
+            s = Normalizer.normalize(s, Normalizer.Form.NFC);
+            this.caseSensitive = caseSensitive;
+            if (regexSearch) {
+                try {
+                    this.searchRegex = Pattern.compile(s, regexFlags(caseSensitive));
+                } catch (PatternSyntaxException e) {
+                    throw new SearchParseError(tr(rxErrorMsg, e.getPattern(), e.getIndex(), e.getMessage()), e);
+                } catch (IllegalArgumentException | StringIndexOutOfBoundsException e) {
+                    // StringIndexOutOfBoundsException catched because of https://bugs.openjdk.java.net/browse/JI-9044959
+                    // See #13870: To remove after we switch to a version of Java which resolves this bug
+                    throw new SearchParseError(tr(rxErrorMsgNoPos, s, e.getMessage()), e);
+                }
+                this.search = s;
+            } else if (caseSensitive) {
+                this.search = s;
+                this.searchRegex = null;
+            } else {
+                this.search = s.toLowerCase(Locale.ENGLISH);
+                this.searchRegex = null;
+            }
+        }
+
+        @Override
+        public boolean match(Tagged osm) {
+            if (!osm.hasKeys())
+                return search.isEmpty();
+
+            for (String key: osm.keySet()) {
+                String value = osm.get(key);
+                if (searchRegex != null) {
+
+                    value = Normalizer.normalize(value, Normalizer.Form.NFC);
+
+                    Matcher keyMatcher = searchRegex.matcher(key);
+                    Matcher valMatcher = searchRegex.matcher(value);
+
+                    boolean keyMatchFound = keyMatcher.find();
+                    boolean valMatchFound = valMatcher.find();
+
+                    if (keyMatchFound || valMatchFound)
+                        return true;
+                } else {
+                    if (!caseSensitive) {
+                        key = key.toLowerCase(Locale.ENGLISH);
+                        value = value.toLowerCase(Locale.ENGLISH);
+                    }
+
+                    value = Normalizer.normalize(value, Normalizer.Form.NFC);
+
+                    if (key.indexOf(search) != -1 || value.indexOf(search) != -1)
+                        return true;
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return search;
+        }
+    }
+
+    private static class ExactType extends Match {
+        private final OsmPrimitiveType type;
+
+        ExactType(String type) throws SearchParseError {
+            this.type = OsmPrimitiveType.from(type);
+            if (this.type == null)
+                throw new SearchParseError(tr("Unknown primitive type: {0}. Allowed values are node, way or relation", type));
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return type.equals(osm.getType());
+        }
+
+        @Override
+        public String toString() {
+            return "type=" + type;
+        }
+    }
+
+    /**
+     * Matches objects last changed by the given username.
+     */
+    private static class UserMatch extends Match {
+        private String user;
+
+        UserMatch(String user) {
+            if ("anonymous".equals(user)) {
+                this.user = null;
+            } else {
+                this.user = user;
+            }
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            if (osm.getUser() == null)
+                return user == null;
+            else
+                return osm.getUser().hasName(user);
+        }
+
+        @Override
+        public String toString() {
+            return "user=" + (user == null ? "" : user);
+        }
+    }
+
+    /**
+     * Matches objects with the given relation role (i.e. "outer").
+     */
+    private static class RoleMatch extends Match {
+        private String role;
+
+        RoleMatch(String role) {
+            if (role == null) {
+                this.role = "";
+            } else {
+                this.role = role;
+            }
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            for (OsmPrimitive ref: osm.getReferrers()) {
+                if (ref instanceof Relation && !ref.isIncomplete() && !ref.isDeleted()) {
+                    for (RelationMember m : ((Relation) ref).getMembers()) {
+                        if (m.getMember() == osm) {
+                            String testRole = m.getRole();
+                            if (role.equals(testRole == null ? "" : testRole))
+                                return true;
+                        }
+                    }
+                }
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "role=" + role;
+        }
+    }
+
+    /**
+     * Matches the n-th object of a relation and/or the n-th node of a way.
+     */
+    private static class Nth extends Match {
+
+        private final int nth;
+        private final boolean modulo;
+
+        Nth(PushbackTokenizer tokenizer, boolean modulo) throws SearchParseError {
+            this((int) tokenizer.readNumber(tr("Positive integer expected")), modulo);
+        }
+
+        private Nth(int nth, boolean modulo) {
+            this.nth = nth;
+            this.modulo = modulo;
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            for (OsmPrimitive p : osm.getReferrers()) {
+                final int idx;
+                final int maxIndex;
+                if (p instanceof Way) {
+                    Way w = (Way) p;
+                    idx = w.getNodes().indexOf(osm);
+                    maxIndex = w.getNodesCount();
+                } else if (p instanceof Relation) {
+                    Relation r = (Relation) p;
+                    idx = r.getMemberPrimitivesList().indexOf(osm);
+                    maxIndex = r.getMembersCount();
+                } else {
+                    continue;
+                }
+                if (nth < 0 && idx - maxIndex == nth) {
+                    return true;
+                } else if (idx == nth || (modulo && idx % nth == 0))
+                    return true;
+            }
+            return false;
+        }
+
+        @Override
+        public String toString() {
+            return "Nth{nth=" + nth + ", modulo=" + modulo + '}';
+        }
+    }
+
+    /**
+     * Matches objects with properties in a certain range.
+     */
+    private abstract static class RangeMatch extends Match {
+
+        private final long min;
+        private final long max;
+
+        RangeMatch(long min, long max) {
+            this.min = Math.min(min, max);
+            this.max = Math.max(min, max);
+        }
+
+        RangeMatch(Range range) {
+            this(range.getStart(), range.getEnd());
+        }
+
+        protected abstract Long getNumber(OsmPrimitive osm);
+
+        protected abstract String getString();
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            Long num = getNumber(osm);
+            if (num == null)
+                return false;
+            else
+                return (num >= min) && (num <= max);
+        }
+
+        @Override
+        public String toString() {
+            return getString() + '=' + min + '-' + max;
+        }
+    }
+
+    /**
+     * Matches ways with a number of nodes in given range
+     */
+    private static class NodeCountRange extends RangeMatch {
+        NodeCountRange(Range range) {
+            super(range);
+        }
+
+        NodeCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of numbers expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            if (osm instanceof Way) {
+                return (long) ((Way) osm).getRealNodesCount();
+            } else if (osm instanceof Relation) {
+                return (long) ((Relation) osm).getMemberPrimitives(Node.class).size();
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        protected String getString() {
+            return "nodes";
+        }
+    }
+
+    /**
+     * Matches objects with the number of referring/contained ways in the given range
+     */
+    private static class WayCountRange extends RangeMatch {
+        WayCountRange(Range range) {
+            super(range);
+        }
+
+        WayCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of numbers expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            if (osm instanceof Node) {
+                return (long) Utils.filteredCollection(osm.getReferrers(), Way.class).size();
+            } else if (osm instanceof Relation) {
+                return (long) ((Relation) osm).getMemberPrimitives(Way.class).size();
+            } else {
+                return null;
+            }
+        }
+
+        @Override
+        protected String getString() {
+            return "ways";
+        }
+    }
+
+    /**
+     * Matches objects with a number of tags in given range
+     */
+    private static class TagCountRange extends RangeMatch {
+        TagCountRange(Range range) {
+            super(range);
+        }
+
+        TagCountRange(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of numbers expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            return (long) osm.getKeys().size();
+        }
+
+        @Override
+        protected String getString() {
+            return "tags";
+        }
+    }
+
+    /**
+     * Matches objects with a timestamp in given range
+     */
+    private static class TimestampRange extends RangeMatch {
+
+        TimestampRange(long minCount, long maxCount) {
+            super(minCount, maxCount);
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            return osm.getTimestamp().getTime();
+        }
+
+        @Override
+        protected String getString() {
+            return "timestamp";
+        }
+    }
+
+    /**
+     * Matches relations with a member of the given role
+     */
+    private static class HasRole extends Match {
+        private final String role;
+
+        HasRole(PushbackTokenizer tokenizer) {
+            role = tokenizer.readTextOrNumber();
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm instanceof Relation && ((Relation) osm).getMemberRoles().contains(role);
+        }
+    }
+
+    /**
+     * Matches objects that are new (i.e. have not been uploaded to the server)
+     */
+    private static class New extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm.isNew();
+        }
+
+        @Override
+        public String toString() {
+            return "new";
+        }
+    }
+
+    /**
+     * Matches all objects that have been modified, created, or undeleted
+     */
+    private static class Modified extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm.isModified() || osm.isNewOrUndeleted();
+        }
+
+        @Override
+        public String toString() {
+            return "modified";
+        }
+    }
+
+    /**
+     * Matches all objects that have been deleted
+     */
+    private static class Deleted extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm.isDeleted();
+        }
+
+        @Override
+        public String toString() {
+            return "deleted";
+        }
+    }
+
+    /**
+     * Matches all objects currently selected
+     */
+    private static class Selected extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm.getDataSet().isSelected(osm);
+        }
+
+        @Override
+        public String toString() {
+            return "selected";
+        }
+    }
+
+    /**
+     * Match objects that are incomplete, where only id and type are known.
+     * Typically some members of a relation are incomplete until they are
+     * fetched from the server.
+     */
+    private static class Incomplete extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm.isIncomplete() || (osm instanceof Relation && ((Relation) osm).hasIncompleteMembers());
+        }
+
+        @Override
+        public String toString() {
+            return "incomplete";
+        }
+    }
+
+    /**
+     * Matches objects that don't have any interesting tags (i.e. only has source,
+     * FIXME, etc.). The complete list of uninteresting tags can be found here:
+     * org.openstreetmap.josm.data.osm.OsmPrimitive.getUninterestingKeys()
+     */
+    private static class Untagged extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return !osm.isTagged() && !osm.isIncomplete();
+        }
+
+        @Override
+        public String toString() {
+            return "untagged";
+        }
+    }
+
+    /**
+     * Matches ways which are closed (i.e. first and last node are the same)
+     */
+    private static class Closed extends Match {
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            return osm instanceof Way && ((Way) osm).isClosed();
+        }
+
+        @Override
+        public String toString() {
+            return "closed";
+        }
+    }
+
+    /**
+     * Matches objects if they are parents of the expression
+     */
+    public static class Parent extends UnaryMatch {
+        public Parent(Match m) {
+            super(m);
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            boolean isParent = false;
+
+            if (osm instanceof Way) {
+                for (Node n : ((Way) osm).getNodes()) {
+                    isParent |= match.match(n);
+                }
+            } else if (osm instanceof Relation) {
+                for (RelationMember member : ((Relation) osm).getMembers()) {
+                    isParent |= match.match(member.getMember());
+                }
+            }
+            return isParent;
+        }
+
+        @Override
+        public String toString() {
+            return "parent(" + match + ')';
+        }
+    }
+
+    /**
+     * Matches objects if they are children of the expression
+     */
+    public static class Child extends UnaryMatch {
+
+        public Child(Match m) {
+            super(m);
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            boolean isChild = false;
+            for (OsmPrimitive p : osm.getReferrers()) {
+                isChild |= match.match(p);
+            }
+            return isChild;
+        }
+
+        @Override
+        public String toString() {
+            return "child(" + match + ')';
+        }
+    }
+
+    /**
+     * Matches if the size of the area is within the given range
+     *
+     * @author Ole Jørgen Brønner
+     */
+    private static class AreaSize extends RangeMatch {
+
+        AreaSize(Range range) {
+            super(range);
+        }
+
+        AreaSize(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of numbers expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            final Double area = Geometry.computeArea(osm);
+            return area == null ? null : area.longValue();
+        }
+
+        @Override
+        protected String getString() {
+            return "areasize";
+        }
+    }
+
+    /**
+     * Matches if the length of a way is within the given range
+     */
+    private static class WayLength extends RangeMatch {
+
+        WayLength(Range range) {
+            super(range);
+        }
+
+        WayLength(PushbackTokenizer tokenizer) throws SearchParseError {
+            this(tokenizer.readRange(tr("Range of numbers expected")));
+        }
+
+        @Override
+        protected Long getNumber(OsmPrimitive osm) {
+            if (!(osm instanceof Way))
+                return null;
+            Way way = (Way) osm;
+            return (long) way.getLength();
+        }
+
+        @Override
+        protected String getString() {
+            return "waylength";
+        }
+    }
+
+    /**
+     * Matches objects within the given bounds.
+     */
+    private abstract static class InArea extends Match {
+
+        protected final boolean all;
+
+        /**
+         * @param all if true, all way nodes or relation members have to be within source area;if false, one suffices.
+         */
+        InArea(boolean all) {
+            this.all = all;
+        }
+
+        protected abstract Collection<Bounds> getBounds(OsmPrimitive primitive);
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            if (!osm.isUsable())
+                return false;
+            else if (osm instanceof Node) {
+                LatLon coordinate = ((Node) osm).getCoor();
+                Collection<Bounds> allBounds = getBounds(osm);
+                return coordinate != null && allBounds != null && allBounds.stream().anyMatch(bounds -> bounds.contains(coordinate));
+            } else if (osm instanceof Way) {
+                Collection<Node> nodes = ((Way) osm).getNodes();
+                return all ? nodes.stream().allMatch(this) : nodes.stream().anyMatch(this);
+            } else if (osm instanceof Relation) {
+                Collection<OsmPrimitive> primitives = ((Relation) osm).getMemberPrimitivesList();
+                return all ? primitives.stream().allMatch(this) : primitives.stream().anyMatch(this);
+            } else
+                return false;
+        }
+    }
+
+    /**
+     * Matches objects within source area ("downloaded area").
+     */
+    public static class InDataSourceArea extends InArea {
+
+        /**
+         * Constructs a new {@code InDataSourceArea}.
+         * @param all if true, all way nodes or relation members have to be within source area; if false, one suffices.
+         */
+        public InDataSourceArea(boolean all) {
+            super(all);
+        }
+
+        @Override
+        protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
+            return primitive.getDataSet() != null ? primitive.getDataSet().getDataSourceBounds() : null;
+        }
+
+        @Override
+        public String toString() {
+            return all ? "allindownloadedarea" : "indownloadedarea";
+        }
+    }
+
+    /**
+     * Matches objects which are not outside the source area ("downloaded area").
+     * Unlike {@link InDataSourceArea} this matches also if no source area is set (e.g., for new layers).
+     */
+    public static class NotOutsideDataSourceArea extends InDataSourceArea {
+
+        /**
+         * Constructs a new {@code NotOutsideDataSourceArea}.
+         */
+        public NotOutsideDataSourceArea() {
+            super(false);
+        }
+
+        @Override
+        protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
+            final Collection<Bounds> bounds = super.getBounds(primitive);
+            return bounds == null || bounds.isEmpty() ? Collections.singleton(Main.getProjection().getWorldBoundsLatLon()) : bounds;
+        }
+
+        @Override
+        public String toString() {
+            return "NotOutsideDataSourceArea";
+        }
+    }
+
+    /**
+     * Matches objects within current map view.
+     */
+    private static class InView extends InArea {
+
+        InView(boolean all) {
+            super(all);
+        }
+
+        @Override
+        protected Collection<Bounds> getBounds(OsmPrimitive primitive) {
+            if (!MainApplication.isDisplayingMapView()) {
+                return null;
+            }
+            return Collections.singleton(MainApplication.getMap().mapView.getRealBounds());
+        }
+
+        @Override
+        public String toString() {
+            return all ? "allinview" : "inview";
+        }
+    }
+
+    /**
+     * Matches presets.
+     * @since 12464
+     */
+    private static class Preset extends Match {
+        private final List<TaggingPreset> presets;
+
+        Preset(String presetName) throws SearchParseError {
+
+            if (presetName == null || presetName.isEmpty()) {
+                throw new SearchParseError("The name of the preset is required");
+            }
+
+            int wildCardIdx = presetName.lastIndexOf('*');
+            int length = presetName.length() - 1;
+
+            /*
+             * Match strictly (simply comparing the names) if there is no '*' symbol
+             * at the end of the name or '*' is a part of the preset name.
+             */
+            boolean matchStrictly = wildCardIdx == -1 || wildCardIdx != length;
+
+            this.presets = TaggingPresets.getTaggingPresets()
+                    .stream()
+                    .filter(preset -> !(preset instanceof TaggingPresetMenu || preset instanceof TaggingPresetSeparator))
+                    .filter(preset -> presetNameMatch(presetName, preset, matchStrictly))
+                    .collect(Collectors.toList());
+
+            if (this.presets.isEmpty()) {
+                throw new SearchParseError(tr("Unknown preset name: ") + presetName);
+            }
+        }
+
+        @Override
+        public boolean match(OsmPrimitive osm) {
+            for (TaggingPreset preset : this.presets) {
+                if (preset.test(osm)) {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        private static boolean presetNameMatch(String name, TaggingPreset preset, boolean matchStrictly) {
+            if (matchStrictly) {
+                return name.equalsIgnoreCase(preset.getRawName());
+            }
+
+            try {
+                String groupSuffix = name.substring(0, name.length() - 2); // try to remove '/*'
+                TaggingPresetMenu group = preset.group;
+
+                return group != null && groupSuffix.equalsIgnoreCase(group.getRawName());
+            } catch (StringIndexOutOfBoundsException ex) {
+                return false;
+            }
+        }
+    }
+
+    /**
+     * Compiles the search expression.
+     * @param searchStr the search expression
+     * @return a {@link Match} object for the expression
+     * @throws SearchParseError if an error has been encountered while compiling
+     * @see #compile(org.openstreetmap.josm.actions.search.SearchAction.SearchSetting)
+     */
+    public static Match compile(String searchStr) throws SearchParseError {
+        return new SearchCompiler(false, false,
+                new PushbackTokenizer(
+                        new PushbackReader(new StringReader(searchStr))))
+                .parse();
+    }
+
+    /**
+     * Compiles the search expression.
+     * @param setting the settings to use
+     * @return a {@link Match} object for the expression
+     * @throws SearchParseError if an error has been encountered while compiling
+     * @see #compile(String)
+     */
+    public static Match compile(SearchAction.SearchSetting setting) throws SearchParseError {
+        if (setting.mapCSSSearch) {
+            return compileMapCSS(setting.text);
+        }
+        return new SearchCompiler(setting.caseSensitive, setting.regexSearch,
+                new PushbackTokenizer(
+                        new PushbackReader(new StringReader(setting.text))))
+                .parse();
+    }
+
+    static Match compileMapCSS(String mapCSS) throws SearchParseError {
+        try {
+            final List<Selector> selectors = new MapCSSParser(new StringReader(mapCSS)).selectors();
+            return new Match() {
+                @Override
+                public boolean match(OsmPrimitive osm) {
+                    for (Selector selector : selectors) {
+                        if (selector.matches(new Environment(osm))) {
+                            return true;
+                        }
+                    }
+                    return false;
+                }
+            };
+        } catch (ParseException e) {
+            throw new SearchParseError(tr("Failed to parse MapCSS selector"), e);
+        }
+    }
+
+    /**
+     * Parse search string.
+     *
+     * @return match determined by search string
+     * @throws org.openstreetmap.josm.data.osm.search.SearchParseError if search expression cannot be parsed
+     */
+    public Match parse() throws SearchParseError {
+        Match m = Optional.ofNullable(parseExpression()).orElse(Always.INSTANCE);
+        if (!tokenizer.readIfEqual(Token.EOF))
+            throw new SearchParseError(tr("Unexpected token: {0}", tokenizer.nextToken()));
+        Logging.debug("Parsed search expression is {0}", m);
+        return m;
+    }
+
+    /**
+     * Parse expression.
+     *
+     * @return match determined by parsing expression
+     * @throws SearchParseError if search expression cannot be parsed
+     */
+    private Match parseExpression() throws SearchParseError {
+        // Step 1: parse the whole expression and build a list of factors and logical tokens
+        List<Object> list = parseExpressionStep1();
+        // Step 2: iterate the list in reverse order to build the logical expression
+        // This iterative approach avoids StackOverflowError for long expressions (see #14217)
+        return parseExpressionStep2(list);
+    }
+
+    private List<Object> parseExpressionStep1() throws SearchParseError {
+        Match factor;
+        String token = null;
+        String errorMessage = null;
+        List<Object> list = new ArrayList<>();
+        do {
+            factor = parseFactor();
+            if (factor != null) {
+                if (token != null) {
+                    list.add(token);
+                }
+                list.add(factor);
+                if (tokenizer.readIfEqual(Token.OR)) {
+                    token = "OR";
+                    errorMessage = tr("Missing parameter for OR");
+                } else if (tokenizer.readIfEqual(Token.XOR)) {
+                    token = "XOR";
+                    errorMessage = tr("Missing parameter for XOR");
+                } else {
+                    token = "AND";
+                    errorMessage = null;
+                }
+            } else if (errorMessage != null) {
+                throw new SearchParseError(errorMessage);
+            }
+        } while (factor != null);
+        return list;
+    }
+
+    private static Match parseExpressionStep2(List<Object> list) {
+        Match result = null;
+        for (int i = list.size() - 1; i >= 0; i--) {
+            Object o = list.get(i);
+            if (o instanceof Match && result == null) {
+                result = (Match) o;
+            } else if (o instanceof String && i > 0) {
+                Match factor = (Match) list.get(i-1);
+                switch ((String) o) {
+                case "OR":
+                    result = new Or(factor, result);
+                    break;
+                case "XOR":
+                    result = new Xor(factor, result);
+                    break;
+                case "AND":
+                    result = new And(factor, result);
+                    break;
+                default: throw new IllegalStateException(tr("Unexpected token: {0}", o));
+                }
+                i--;
+            } else {
+                throw new IllegalStateException("i=" + i + "; o=" + o);
+            }
+        }
+        return result;
+    }
+
+    /**
+     * Parse next factor (a search operator or search term).
+     *
+     * @return match determined by parsing factor string
+     * @throws SearchParseError if search expression cannot be parsed
+     */
+    private Match parseFactor() throws SearchParseError {
+        if (tokenizer.readIfEqual(Token.LEFT_PARENT)) {
+            Match expression = parseExpression();
+            if (!tokenizer.readIfEqual(Token.RIGHT_PARENT))
+                throw new SearchParseError(Token.RIGHT_PARENT, tokenizer.nextToken());
+            return expression;
+        } else if (tokenizer.readIfEqual(Token.NOT)) {
+            return new Not(parseFactor(tr("Missing operator for NOT")));
+        } else if (tokenizer.readIfEqual(Token.KEY)) {
+            // factor consists of key:value or key=value
+            String key = tokenizer.getText();
+            if (tokenizer.readIfEqual(Token.EQUALS)) {
+                return new ExactKeyValue(regexSearch, key, tokenizer.readTextOrNumber());
+            } else if (tokenizer.readIfEqual(Token.LESS_THAN)) {
+                return new ValueComparison(key, tokenizer.readTextOrNumber(), -1);
+            } else if (tokenizer.readIfEqual(Token.GREATER_THAN)) {
+                return new ValueComparison(key, tokenizer.readTextOrNumber(), +1);
+            } else if (tokenizer.readIfEqual(Token.COLON)) {
+                // see if we have a Match that takes a data parameter
+                SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
+                if (factory != null)
+                    return factory.get(key, tokenizer);
+
+                UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
+                if (unaryFactory != null)
+                    return unaryFactory.get(key, parseFactor(), tokenizer);
+
+                // key:value form where value is a string (may be OSM key search)
+                final String value = tokenizer.readTextOrNumber();
+                return new KeyValue(key, value != null ? value : "", regexSearch, caseSensitive);
+            } else if (tokenizer.readIfEqual(Token.QUESTION_MARK))
+                return new BooleanMatch(key, false);
+            else {
+                SimpleMatchFactory factory = simpleMatchFactoryMap.get(key);
+                if (factory != null)
+                    return factory.get(key, null);
+
+                UnaryMatchFactory unaryFactory = unaryMatchFactoryMap.get(key);
+                if (unaryFactory != null)
+                    return unaryFactory.get(key, parseFactor(), null);
+
+                // match string in any key or value
+                return new Any(key, regexSearch, caseSensitive);
+            }
+        } else
+            return null;
+    }
+
+    private Match parseFactor(String errorMessage) throws SearchParseError {
+        return Optional.ofNullable(parseFactor()).orElseThrow(() -> new SearchParseError(errorMessage));
+    }
+
+    private static int regexFlags(boolean caseSensitive) {
+        int searchFlags = 0;
+
+        // Enables canonical Unicode equivalence so that e.g. the two
+        // forms of "\u00e9gal" and "e\u0301gal" will match.
+        //
+        // It makes sense to match no matter how the character
+        // happened to be constructed.
+        searchFlags |= Pattern.CANON_EQ;
+
+        // Make "." match any character including newline (/s in Perl)
+        searchFlags |= Pattern.DOTALL;
+
+        // CASE_INSENSITIVE by itself only matches US-ASCII case
+        // insensitively, but the OSM data is in Unicode. With
+        // UNICODE_CASE casefolding is made Unicode-aware.
+        if (!caseSensitive) {
+            searchFlags |= (Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
+        }
+
+        return searchFlags;
+    }
+
+    static String escapeStringForSearch(String s) {
+        return s.replace("\\", "\\\\").replace("\"", "\\\"");
+    }
+
+    /**
+     * Builds a search string for the given tag. If value is empty, the existence of the key is checked.
+     *
+     * @param key   the tag key
+     * @param value the tag value
+     * @return a search string for the given tag
+     */
+    public static String buildSearchStringForTag(String key, String value) {
+        final String forKey = '"' + escapeStringForSearch(key) + '"' + '=';
+        if (value == null || value.isEmpty()) {
+            return forKey + '*';
+        } else {
+            return forKey + '"' + escapeStringForSearch(value) + '"';
+        }
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/osm/search/SearchParseError.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/search/SearchParseError.java	(revision 12656)
+++ /trunk/src/org/openstreetmap/josm/data/osm/search/SearchParseError.java	(revision 12656)
@@ -0,0 +1,39 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.search;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import org.openstreetmap.josm.data.osm.search.PushbackTokenizer.Token;
+
+/**
+ * Search compiler parsing error.
+ * @since 12656 (extracted from {@link SearchCompiler}).
+ */
+public class SearchParseError extends Exception {
+
+    /**
+     * Constructs a new generic {@code ParseError}.
+     * @param msg the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
+     */
+    public SearchParseError(String msg) {
+        super(msg);
+    }
+
+    /**
+     * Constructs a new generic {@code ParseError}.
+     * @param msg the detail message. The detail message is saved for later retrieval by the {@link #getMessage()} method.
+     * @param  cause the cause (which is saved for later retrieval by the {@link #getCause()} method).
+     */
+    public SearchParseError(String msg, Throwable cause) {
+        super(msg, cause);
+    }
+
+    /**
+     * Constructs a new detailed {@code ParseError}.
+     * @param expected expected token
+     * @param found actual token
+     */
+    public SearchParseError(Token expected, Token found) {
+        this(tr("Unexpected token. Expected {0}, found {1}", expected, found));
+    }
+}
Index: /trunk/src/org/openstreetmap/josm/data/osm/search/package-info.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/osm/search/package-info.java	(revision 12656)
+++ /trunk/src/org/openstreetmap/josm/data/osm/search/package-info.java	(revision 12656)
@@ -0,0 +1,6 @@
+// License: GPL. For details, see LICENSE file.
+
+/**
+ * Provides classes allowing to search OSM primitives in a dataset using textual queries.
+ */
+package org.openstreetmap.josm.data.osm.search;
Index: /trunk/src/org/openstreetmap/josm/data/validation/Test.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/data/validation/Test.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/data/validation/Test.java	(revision 12656)
@@ -15,5 +15,4 @@
 import javax.swing.JPanel;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.NotOutsideDataSourceArea;
 import org.openstreetmap.josm.command.Command;
 import org.openstreetmap.josm.command.DeleteCommand;
@@ -22,4 +21,5 @@
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.NotOutsideDataSourceArea;
 import org.openstreetmap.josm.data.osm.visitor.AbstractVisitor;
 import org.openstreetmap.josm.gui.MainApplication;
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/RelationListDialog.java	(revision 12656)
@@ -41,5 +41,4 @@
 import org.openstreetmap.josm.actions.relation.SelectMembersAction;
 import org.openstreetmap.josm.actions.relation.SelectRelationAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -50,4 +49,5 @@
 import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
 import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.event.NodeMovedEvent;
 import org.openstreetmap.josm.data.osm.event.PrimitivesAddedEvent;
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/PropertiesDialog.java	(revision 12656)
@@ -61,5 +61,4 @@
 import org.openstreetmap.josm.actions.relation.SelectRelationAction;
 import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.command.ChangeCommand;
 import org.openstreetmap.josm.command.ChangePropertyCommand;
@@ -78,4 +77,5 @@
 import org.openstreetmap.josm.data.osm.event.DatasetEventManager;
 import org.openstreetmap.josm.data.osm.event.DatasetEventManager.FireMode;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.event.SelectionEventManager;
 import org.openstreetmap.josm.data.preferences.StringProperty;
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/RecentTagCollection.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/RecentTagCollection.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/RecentTagCollection.java	(revision 12656)
@@ -9,6 +9,7 @@
 
 import org.openstreetmap.josm.actions.search.SearchAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.preferences.CollectionProperty;
 
@@ -81,9 +82,9 @@
     }
 
-    public void setTagsToIgnore(SearchAction.SearchSetting tagsToIgnore) throws SearchCompiler.ParseError {
+    public void setTagsToIgnore(SearchAction.SearchSetting tagsToIgnore) throws SearchParseError {
         setTagsToIgnore(tagsToIgnore.text.isEmpty() ? SearchCompiler.Never.INSTANCE : SearchCompiler.compile(tagsToIgnore));
     }
 
-    public SearchAction.SearchSetting ignoreTag(Tag tagToIgnore, SearchAction.SearchSetting settingToUpdate) throws SearchCompiler.ParseError {
+    public SearchAction.SearchSetting ignoreTag(Tag tagToIgnore, SearchAction.SearchSetting settingToUpdate) throws SearchParseError {
         final String forTag = SearchCompiler.buildSearchStringForTag(tagToIgnore.getKey(), tagToIgnore.getValue());
         settingToUpdate.text = settingToUpdate.text.isEmpty()
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/SearchBasedRowFilter.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/SearchBasedRowFilter.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/SearchBasedRowFilter.java	(revision 12656)
@@ -5,10 +5,10 @@
 import javax.swing.table.TableModel;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 
 /**
  * A {@link RowFilter} implementation which matches tags w.r.t. the specified filter's
- * {@link org.openstreetmap.josm.actions.search.SearchCompiler.Match#match(org.openstreetmap.josm.data.osm.Tagged)} method.
+ * {@link org.openstreetmap.josm.data.osm.search.SearchCompiler.Match#match(org.openstreetmap.josm.data.osm.Tagged)} method.
  *
  * <p>An {@link javax.swing.RowFilter.Entry}'s column 0 is considered as key, and column 1 is considered as value.</p>
Index: /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/dialogs/properties/TagEditHelper.java	(revision 12656)
@@ -64,5 +64,4 @@
 import org.openstreetmap.josm.actions.JosmAction;
 import org.openstreetmap.josm.actions.search.SearchAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.command.ChangePropertyCommand;
 import org.openstreetmap.josm.command.Command;
@@ -70,4 +69,6 @@
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.preferences.BooleanProperty;
 import org.openstreetmap.josm.data.preferences.CollectionProperty;
@@ -335,5 +336,5 @@
                 tagsToIgnore = searchSetting;
                 recentTags.setTagsToIgnore(tagsToIgnore);
-            } catch (SearchCompiler.ParseError parseError) {
+            } catch (SearchParseError parseError) {
                 warnAboutParseError(parseError);
                 tagsToIgnore = new SearchAction.SearchSetting();
@@ -343,5 +344,5 @@
     }
 
-    private static void warnAboutParseError(SearchCompiler.ParseError parseError) {
+    private static void warnAboutParseError(SearchParseError parseError) {
         Logging.warn(parseError);
         JOptionPane.showMessageDialog(
@@ -1009,5 +1010,5 @@
                         PROPERTY_TAGS_TO_IGNORE.put(tagsToIgnore.writeToString());
                     }
-                } catch (SearchCompiler.ParseError parseError) {
+                } catch (SearchParseError parseError) {
                     throw new IllegalStateException(parseError);
                 }
@@ -1031,5 +1032,5 @@
                     recentTags.setTagsToIgnore(tagsToIgnore);
                     PROPERTY_TAGS_TO_IGNORE.put(tagsToIgnore.writeToString());
-                } catch (SearchCompiler.ParseError parseError) {
+                } catch (SearchParseError parseError) {
                     warnAboutParseError(parseError);
                 }
Index: /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/layer/markerlayer/Marker.java	(revision 12656)
@@ -23,5 +23,4 @@
 import javax.swing.ImageIcon;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.Preferences.PreferenceChangeEvent;
 import org.openstreetmap.josm.data.coor.CachedLatLon;
@@ -30,4 +29,5 @@
 import org.openstreetmap.josm.data.gpx.GpxConstants;
 import org.openstreetmap.josm.data.gpx.WayPoint;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.preferences.CachedProperty;
 import org.openstreetmap.josm.data.preferences.IntegerProperty;
Index: /trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ConditionFactory.java	(revision 12656)
@@ -15,5 +15,4 @@
 import java.util.regex.PatternSyntaxException;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.InDataSourceArea;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -22,4 +21,5 @@
 import org.openstreetmap.josm.data.osm.Tag;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.InDataSourceArea;
 import org.openstreetmap.josm.data.osm.visitor.paint.relations.MultipolygonCache;
 import org.openstreetmap.josm.gui.mappaint.Cascade;
Index: /trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ExpressionFactory.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ExpressionFactory.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/mappaint/mapcss/ExpressionFactory.java	(revision 12656)
@@ -24,11 +24,11 @@
 import java.util.zip.CRC32;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 import org.openstreetmap.josm.gui.mappaint.Cascade;
 import org.openstreetmap.josm.gui.mappaint.Environment;
@@ -676,5 +676,5 @@
             try {
                 m = SearchCompiler.compile(searchStr);
-            } catch (ParseError ex) {
+            } catch (SearchParseError ex) {
                 Logging.trace(ex);
                 return null;
Index: /trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/tagging/presets/TaggingPreset.java	(revision 12656)
@@ -34,6 +34,4 @@
 import org.openstreetmap.josm.Main;
 import org.openstreetmap.josm.actions.AdaptableAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
 import org.openstreetmap.josm.command.ChangePropertyCommand;
 import org.openstreetmap.josm.command.Command;
@@ -44,4 +42,7 @@
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.gui.ExtendedDialog;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -246,5 +247,5 @@
         try {
             this.nameTemplateFilter = SearchCompiler.compile(filter);
-        } catch (SearchCompiler.ParseError e) {
+        } catch (SearchParseError e) {
             Logging.error("Error while parsing" + filter + ": " + e.getMessage());
             throw new SAXException(e);
Index: /trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/tagging/presets/items/Roles.java	(revision 12656)
@@ -14,7 +14,8 @@
 
 import org.openstreetmap.josm.actions.search.SearchAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetItem;
 import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
@@ -82,5 +83,5 @@
                 searchSetting.regexSearch = true;
                 this.memberExpression = SearchCompiler.compile(searchSetting);
-            } catch (SearchCompiler.ParseError ex) {
+            } catch (SearchParseError ex) {
                 throw new SAXException(tr("Illegal member expression: {0}", ex.getMessage()), ex);
             }
Index: /trunk/src/org/openstreetmap/josm/gui/widgets/CompileSearchTextDecorator.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/gui/widgets/CompileSearchTextDecorator.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/gui/widgets/CompileSearchTextDecorator.java	(revision 12656)
@@ -9,5 +9,6 @@
 import javax.swing.text.JTextComponent;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.tools.Logging;
 
@@ -43,5 +44,5 @@
             textComponent.setToolTipText(originalToolTipText);
             filter = SearchCompiler.compile(textComponent.getText());
-        } catch (SearchCompiler.ParseError ex) {
+        } catch (SearchParseError ex) {
             textComponent.setBackground(new Color(255, 224, 224));
             textComponent.setToolTipText(ex.getMessage());
Index: /trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/io/remotecontrol/handler/LoadAndZoomHandler.java	(revision 12656)
@@ -16,5 +16,4 @@
 import org.openstreetmap.josm.actions.downloadtasks.DownloadTask;
 import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.Bounds;
 import org.openstreetmap.josm.data.coor.LatLon;
@@ -24,4 +23,6 @@
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.SimplePrimitiveId;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.visitor.BoundingXYVisitor;
 import org.openstreetmap.josm.gui.MainApplication;
@@ -202,5 +203,5 @@
                     zoom(filteredPrimitives, bbox);
                 });
-            } catch (SearchCompiler.ParseError ex) {
+            } catch (SearchParseError ex) {
                 Logging.error(ex);
                 throw new RequestHandlerErrorException(ex);
Index: /trunk/src/org/openstreetmap/josm/tools/template_engine/ContextSwitchTemplate.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/template_engine/ContextSwitchTemplate.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/tools/template_engine/ContextSwitchTemplate.java	(revision 12656)
@@ -9,10 +9,4 @@
 import java.util.List;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.And;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Child;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Not;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Or;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Parent;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
@@ -20,4 +14,10 @@
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.And;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Child;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Not;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Or;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Parent;
 
 /**
Index: /trunk/src/org/openstreetmap/josm/tools/template_engine/ParseError.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/template_engine/ParseError.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/tools/template_engine/ParseError.java	(revision 12656)
@@ -4,4 +4,5 @@
 import static org.openstreetmap.josm.tools.I18n.tr;
 
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.tools.template_engine.Tokenizer.Token;
 import org.openstreetmap.josm.tools.template_engine.Tokenizer.TokenType;
@@ -22,5 +23,5 @@
     }
 
-    public ParseError(int position, org.openstreetmap.josm.actions.search.SearchCompiler.ParseError e) {
+    public ParseError(int position, SearchParseError e) {
         super(tr("Error while parsing search expression on position {0}", position), e);
         unexpectedToken = null;
Index: /trunk/src/org/openstreetmap/josm/tools/template_engine/SearchExpressionCondition.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/template_engine/SearchExpressionCondition.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/tools/template_engine/SearchExpressionCondition.java	(revision 12656)
@@ -2,5 +2,5 @@
 package org.openstreetmap.josm.tools.template_engine;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 
 public class SearchExpressionCondition implements TemplateEntry {
Index: /trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEngineDataProvider.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEngineDataProvider.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateEngineDataProvider.java	(revision 12656)
@@ -4,5 +4,5 @@
 import java.util.Collection;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
 
 public interface TemplateEngineDataProvider {
Index: /trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateParser.java
===================================================================
--- /trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateParser.java	(revision 12655)
+++ /trunk/src/org/openstreetmap/josm/tools/template_engine/TemplateParser.java	(revision 12656)
@@ -9,6 +9,7 @@
 import java.util.List;
 
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.tools.template_engine.Tokenizer.Token;
 import org.openstreetmap.josm.tools.template_engine.Tokenizer.TokenType;
@@ -102,5 +103,5 @@
                     result.getEntries().add(new SearchExpressionCondition(
                             SearchCompiler.compile(searchText), condition));
-                } catch (SearchCompiler.ParseError e) {
+                } catch (SearchParseError e) {
                     throw new ParseError(searchExpression.getPosition(), e);
                 }
@@ -133,5 +134,5 @@
                 Match match = SearchCompiler.compile(searchText);
                 result = new ContextSwitchTemplate(match, template, searchExpression.getPosition());
-            } catch (SearchCompiler.ParseError e) {
+            } catch (SearchParseError e) {
                 throw new ParseError(searchExpression.getPosition(), e);
             }
Index: /trunk/test/functional/org/openstreetmap/josm/data/BoundariesTestIT.java
===================================================================
--- /trunk/test/functional/org/openstreetmap/josm/data/BoundariesTestIT.java	(revision 12655)
+++ /trunk/test/functional/org/openstreetmap/josm/data/BoundariesTestIT.java	(revision 12656)
@@ -12,7 +12,7 @@
 
 import org.junit.Test;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.io.OsmReader;
 
Index: /trunk/test/unit/org/openstreetmap/josm/actions/CreateMultipolygonActionTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/actions/CreateMultipolygonActionTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/actions/CreateMultipolygonActionTest.java	(revision 12656)
@@ -13,6 +13,4 @@
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.command.SequenceCommand;
 import org.openstreetmap.josm.data.osm.DataSet;
@@ -20,4 +18,6 @@
 import org.openstreetmap.josm.data.osm.RelationMember;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.io.OsmReader;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
@@ -56,5 +56,5 @@
     @SuppressWarnings("unchecked")
     private static Pair<SequenceCommand, Relation> createMultipolygonCommand(Collection<Way> ways, String pattern, Relation r)
-            throws ParseError {
+            throws SearchParseError {
         return CreateMultipolygonAction.createMultipolygonCommand(
             (Collection<Way>) (Collection<?>) SubclassFilteredCollection.filter(ways, SearchCompiler.compile(regexpSearch(pattern))), r);
Index: /trunk/test/unit/org/openstreetmap/josm/actions/OrthogonalizeActionTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/actions/OrthogonalizeActionTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/actions/OrthogonalizeActionTest.java	(revision 12656)
@@ -12,9 +12,9 @@
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.actions.OrthogonalizeAction.Direction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.gui.MainApplication;
 import org.openstreetmap.josm.gui.layer.OsmDataLayer;
Index: unk/test/unit/org/openstreetmap/josm/actions/search/PushbackTokenizerTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/actions/search/PushbackTokenizerTest.java	(revision 12655)
+++ 	(revision )
@@ -1,31 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.actions.search;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.actions.search.PushbackTokenizer.Token;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests for class {@link SearchCompiler}.
- */
-public class PushbackTokenizerTest {
-
-    /**
-     * Setup rules.
-     */
-    @Rule
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules();
-
-    /**
-     * Unit test of {@link Token} enum.
-     */
-    @Test
-    public void testEnumToken() {
-        TestUtils.superficialEnumCodeCoverage(Token.class);
-    }
-}
Index: /trunk/test/unit/org/openstreetmap/josm/actions/search/SearchActionTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/actions/search/SearchActionTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/actions/search/SearchActionTest.java	(revision 12656)
@@ -6,4 +6,5 @@
 import org.openstreetmap.josm.TestUtils;
 import org.openstreetmap.josm.actions.search.SearchAction.SearchMode;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
 
Index: unk/test/unit/org/openstreetmap/josm/actions/search/SearchCompilerTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/actions/search/SearchCompilerTest.java	(revision 12655)
+++ 	(revision )
@@ -1,691 +1,0 @@
-// License: GPL. For details, see LICENSE file.
-package org.openstreetmap.josm.actions.search;
-
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertTrue;
-import static org.junit.Assert.fail;
-
-import java.lang.reflect.Field;
-import java.nio.charset.StandardCharsets;
-import java.nio.file.Files;
-import java.nio.file.Paths;
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.Collections;
-
-import org.junit.Rule;
-import org.junit.Test;
-import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ExactKeyValue;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
-import org.openstreetmap.josm.data.coor.LatLon;
-import org.openstreetmap.josm.data.osm.DataSet;
-import org.openstreetmap.josm.data.osm.Node;
-import org.openstreetmap.josm.data.osm.OsmPrimitive;
-import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
-import org.openstreetmap.josm.data.osm.Relation;
-import org.openstreetmap.josm.data.osm.RelationData;
-import org.openstreetmap.josm.data.osm.RelationMember;
-import org.openstreetmap.josm.data.osm.Tag;
-import org.openstreetmap.josm.data.osm.User;
-import org.openstreetmap.josm.data.osm.Way;
-import org.openstreetmap.josm.data.osm.WayData;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
-import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
-import org.openstreetmap.josm.gui.tagging.presets.items.Key;
-import org.openstreetmap.josm.testutils.JOSMTestRules;
-import org.openstreetmap.josm.tools.date.DateUtils;
-
-import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
-
-/**
- * Unit tests for class {@link SearchCompiler}.
- */
-public class SearchCompilerTest {
-
-    /**
-     * We need prefs for this. We access preferences when creating OSM primitives.
-     */
-    @Rule
-    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
-    public JOSMTestRules test = new JOSMTestRules().preferences();
-
-    private static final class SearchContext {
-        final DataSet ds = new DataSet();
-        final Node n1 = new Node(LatLon.ZERO);
-        final Node n2 = new Node(new LatLon(5, 5));
-        final Way w1 = new Way();
-        final Way w2 = new Way();
-        final Relation r1 = new Relation();
-        final Relation r2 = new Relation();
-
-        private final Match m;
-        private final Match n;
-
-        private SearchContext(String state) throws ParseError {
-            m = SearchCompiler.compile(state);
-            n = SearchCompiler.compile('-' + state);
-            ds.addPrimitive(n1);
-            ds.addPrimitive(n2);
-            w1.addNode(n1);
-            w1.addNode(n2);
-            w2.addNode(n1);
-            w2.addNode(n2);
-            ds.addPrimitive(w1);
-            ds.addPrimitive(w2);
-            r1.addMember(new RelationMember("", w1));
-            r1.addMember(new RelationMember("", w2));
-            r2.addMember(new RelationMember("", w1));
-            r2.addMember(new RelationMember("", w2));
-            ds.addPrimitive(r1);
-            ds.addPrimitive(r2);
-        }
-
-        private void match(OsmPrimitive p, boolean cond) {
-            if (cond) {
-                assertTrue(p.toString(), m.match(p));
-                assertFalse(p.toString(), n.match(p));
-            } else {
-                assertFalse(p.toString(), m.match(p));
-                assertTrue(p.toString(), n.match(p));
-            }
-        }
-    }
-
-    private static OsmPrimitive newPrimitive(String key, String value) {
-        final Node p = new Node();
-        p.put(key, value);
-        return p;
-    }
-
-    /**
-     * Search anything.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testAny() throws ParseError {
-        final SearchCompiler.Match c = SearchCompiler.compile("foo");
-        assertTrue(c.match(newPrimitive("foobar", "true")));
-        assertTrue(c.match(newPrimitive("name", "hello-foo-xy")));
-        assertFalse(c.match(newPrimitive("name", "X")));
-        assertEquals("foo", c.toString());
-    }
-
-    /**
-     * Search by equality key=value.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testEquals() throws ParseError {
-        final SearchCompiler.Match c = SearchCompiler.compile("foo=bar");
-        assertFalse(c.match(newPrimitive("foobar", "true")));
-        assertTrue(c.match(newPrimitive("foo", "bar")));
-        assertFalse(c.match(newPrimitive("fooX", "bar")));
-        assertFalse(c.match(newPrimitive("foo", "barX")));
-        assertEquals("foo=bar", c.toString());
-    }
-
-    /**
-     * Search by comparison.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testCompare() throws ParseError {
-        final SearchCompiler.Match c1 = SearchCompiler.compile("start_date>1950");
-        assertTrue(c1.match(newPrimitive("start_date", "1950-01-01")));
-        assertTrue(c1.match(newPrimitive("start_date", "1960")));
-        assertFalse(c1.match(newPrimitive("start_date", "1950")));
-        assertFalse(c1.match(newPrimitive("start_date", "1000")));
-        assertTrue(c1.match(newPrimitive("start_date", "101010")));
-
-        final SearchCompiler.Match c2 = SearchCompiler.compile("start_date<1960");
-        assertTrue(c2.match(newPrimitive("start_date", "1950-01-01")));
-        assertFalse(c2.match(newPrimitive("start_date", "1960")));
-        assertTrue(c2.match(newPrimitive("start_date", "1950")));
-        assertTrue(c2.match(newPrimitive("start_date", "1000")));
-        assertTrue(c2.match(newPrimitive("start_date", "200")));
-
-        final SearchCompiler.Match c3 = SearchCompiler.compile("name<I");
-        assertTrue(c3.match(newPrimitive("name", "Alpha")));
-        assertFalse(c3.match(newPrimitive("name", "Sigma")));
-
-        final SearchCompiler.Match c4 = SearchCompiler.compile("\"start_date\"<1960");
-        assertTrue(c4.match(newPrimitive("start_date", "1950-01-01")));
-        assertFalse(c4.match(newPrimitive("start_date", "2000")));
-
-        final SearchCompiler.Match c5 = SearchCompiler.compile("height>180");
-        assertTrue(c5.match(newPrimitive("height", "200")));
-        assertTrue(c5.match(newPrimitive("height", "99999")));
-        assertFalse(c5.match(newPrimitive("height", "50")));
-        assertFalse(c5.match(newPrimitive("height", "-9999")));
-        assertFalse(c5.match(newPrimitive("height", "fixme")));
-
-        final SearchCompiler.Match c6 = SearchCompiler.compile("name>C");
-        assertTrue(c6.match(newPrimitive("name", "Delta")));
-        assertFalse(c6.match(newPrimitive("name", "Alpha")));
-    }
-
-    /**
-     * Search by nth.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testNth() throws ParseError {
-        final DataSet dataSet = new DataSet();
-        final Way way = new Way();
-        final Node node0 = new Node(new LatLon(1, 1));
-        final Node node1 = new Node(new LatLon(2, 2));
-        final Node node2 = new Node(new LatLon(3, 3));
-        dataSet.addPrimitive(way);
-        dataSet.addPrimitive(node0);
-        dataSet.addPrimitive(node1);
-        dataSet.addPrimitive(node2);
-        way.addNode(node0);
-        way.addNode(node1);
-        way.addNode(node2);
-        assertFalse(SearchCompiler.compile("nth:2").match(node1));
-        assertTrue(SearchCompiler.compile("nth:1").match(node1));
-        assertFalse(SearchCompiler.compile("nth:0").match(node1));
-        assertTrue(SearchCompiler.compile("nth:0").match(node0));
-        assertTrue(SearchCompiler.compile("nth:2").match(node2));
-        assertTrue(SearchCompiler.compile("nth:-1").match(node2));
-        assertTrue(SearchCompiler.compile("nth:-2").match(node1));
-        assertTrue(SearchCompiler.compile("nth:-3").match(node0));
-    }
-
-    /**
-     * Search by negative nth.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testNthParseNegative() throws ParseError {
-        assertEquals("Nth{nth=-1, modulo=false}", SearchCompiler.compile("nth:-1").toString());
-    }
-
-    /**
-     * Search by modified status.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testModified() throws ParseError {
-        SearchContext sc = new SearchContext("modified");
-        // Not modified but new
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            assertFalse(p.toString(), p.isModified());
-            assertTrue(p.toString(), p.isNewOrUndeleted());
-            sc.match(p, true);
-        }
-        // Modified and new
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            p.setModified(true);
-            assertTrue(p.toString(), p.isModified());
-            assertTrue(p.toString(), p.isNewOrUndeleted());
-            sc.match(p, true);
-        }
-        // Modified but not new
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            p.setOsmId(1, 1);
-            assertTrue(p.toString(), p.isModified());
-            assertFalse(p.toString(), p.isNewOrUndeleted());
-            sc.match(p, true);
-        }
-        // Not modified nor new
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
-            p.setOsmId(2, 2);
-            assertFalse(p.toString(), p.isModified());
-            assertFalse(p.toString(), p.isNewOrUndeleted());
-            sc.match(p, false);
-        }
-    }
-
-    /**
-     * Search by selected status.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testSelected() throws ParseError {
-        SearchContext sc = new SearchContext("selected");
-        // Not selected
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            assertFalse(p.toString(), p.isSelected());
-            sc.match(p, false);
-        }
-        // Selected
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
-            sc.ds.addSelected(p);
-            assertTrue(p.toString(), p.isSelected());
-            sc.match(p, true);
-        }
-    }
-
-    /**
-     * Search by incomplete status.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testIncomplete() throws ParseError {
-        SearchContext sc = new SearchContext("incomplete");
-        // Not incomplete
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            assertFalse(p.toString(), p.isIncomplete());
-            sc.match(p, false);
-        }
-        // Incomplete
-        sc.n2.setCoor(null);
-        WayData wd = new WayData();
-        wd.setIncomplete(true);
-        sc.w2.load(wd);
-        RelationData rd = new RelationData();
-        rd.setIncomplete(true);
-        sc.r2.load(rd);
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
-            assertTrue(p.toString(), p.isIncomplete());
-            sc.match(p, true);
-        }
-    }
-
-    /**
-     * Search by untagged status.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testUntagged() throws ParseError {
-        SearchContext sc = new SearchContext("untagged");
-        // Untagged
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            assertFalse(p.toString(), p.isTagged());
-            sc.match(p, true);
-        }
-        // Tagged
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
-            p.put("foo", "bar");
-            assertTrue(p.toString(), p.isTagged());
-            sc.match(p, false);
-        }
-    }
-
-    /**
-     * Search by closed status.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testClosed() throws ParseError {
-        SearchContext sc = new SearchContext("closed");
-        // Closed
-        sc.w1.addNode(sc.n1);
-        for (Way w : new Way[]{sc.w1}) {
-            assertTrue(w.toString(), w.isClosed());
-            sc.match(w, true);
-        }
-        // Unclosed
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w2, sc.r1, sc.r2}) {
-            sc.match(p, false);
-        }
-    }
-
-    /**
-     * Search by new status.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testNew() throws ParseError {
-        SearchContext sc = new SearchContext("new");
-        // New
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
-            assertTrue(p.toString(), p.isNew());
-            sc.match(p, true);
-        }
-        // Not new
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
-            p.setOsmId(2, 2);
-            assertFalse(p.toString(), p.isNew());
-            sc.match(p, false);
-        }
-    }
-
-    /**
-     * Search for node objects.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testTypeNode() throws ParseError {
-        final SearchContext sc = new SearchContext("type:node");
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
-            sc.match(p, OsmPrimitiveType.NODE.equals(p.getType()));
-        }
-    }
-
-    /**
-     * Search for way objects.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testTypeWay() throws ParseError {
-        final SearchContext sc = new SearchContext("type:way");
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
-            sc.match(p, OsmPrimitiveType.WAY.equals(p.getType()));
-        }
-    }
-
-    /**
-     * Search for relation objects.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testTypeRelation() throws ParseError {
-        final SearchContext sc = new SearchContext("type:relation");
-        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
-            sc.match(p, OsmPrimitiveType.RELATION.equals(p.getType()));
-        }
-    }
-
-    /**
-     * Search for users.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testUser() throws ParseError {
-        final SearchContext foobar = new SearchContext("user:foobar");
-        foobar.n1.setUser(User.createLocalUser("foobar"));
-        foobar.match(foobar.n1, true);
-        foobar.match(foobar.n2, false);
-        final SearchContext anonymous = new SearchContext("user:anonymous");
-        anonymous.n1.setUser(User.createLocalUser("foobar"));
-        anonymous.match(anonymous.n1, false);
-        anonymous.match(anonymous.n2, true);
-    }
-
-    /**
-     * Compiles "foo type bar" and tests the parse error message
-     */
-    @Test
-    public void testFooTypeBar() {
-        try {
-            SearchCompiler.compile("foo type bar");
-            fail();
-        } catch (ParseError parseError) {
-            assertEquals("<html>Expecting <code>:</code> after <i>type</i>", parseError.getMessage());
-        }
-    }
-
-    /**
-     * Search for primitive timestamps.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testTimestamp() throws ParseError {
-        final Match search = SearchCompiler.compile("timestamp:2010/2011");
-        final Node n1 = new Node();
-        n1.setTimestamp(DateUtils.fromString("2010-01-22"));
-        assertTrue(search.match(n1));
-        n1.setTimestamp(DateUtils.fromString("2016-01-22"));
-        assertFalse(search.match(n1));
-    }
-
-    /**
-     * Tests the implementation of the Boolean logic.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testBooleanLogic() throws ParseError {
-        final SearchCompiler.Match c1 = SearchCompiler.compile("foo AND bar AND baz");
-        assertTrue(c1.match(newPrimitive("foobar", "baz")));
-        assertEquals("foo && bar && baz", c1.toString());
-        final SearchCompiler.Match c2 = SearchCompiler.compile("foo AND (bar OR baz)");
-        assertTrue(c2.match(newPrimitive("foobar", "yes")));
-        assertTrue(c2.match(newPrimitive("foobaz", "yes")));
-        assertEquals("foo && (bar || baz)", c2.toString());
-        final SearchCompiler.Match c3 = SearchCompiler.compile("foo OR (bar baz)");
-        assertEquals("foo || (bar && baz)", c3.toString());
-        final SearchCompiler.Match c4 = SearchCompiler.compile("foo1 OR (bar1 bar2 baz1 XOR baz2) OR foo2");
-        assertEquals("foo1 || (bar1 && bar2 && (baz1 ^ baz2)) || foo2", c4.toString());
-        final SearchCompiler.Match c5 = SearchCompiler.compile("foo1 XOR (baz1 XOR (bar baz))");
-        assertEquals("foo1 ^ baz1 ^ (bar && baz)", c5.toString());
-        final SearchCompiler.Match c6 = SearchCompiler.compile("foo1 XOR ((baz1 baz2) XOR (bar OR baz))");
-        assertEquals("foo1 ^ (baz1 && baz2) ^ (bar || baz)", c6.toString());
-    }
-
-    /**
-     * Tests {@code buildSearchStringForTag}.
-     * @throws ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testBuildSearchStringForTag() throws ParseError {
-        final Tag tag1 = new Tag("foo=", "bar\"");
-        final Tag tag2 = new Tag("foo=", "=bar");
-        final String search1 = SearchCompiler.buildSearchStringForTag(tag1.getKey(), tag1.getValue());
-        assertEquals("\"foo=\"=\"bar\\\"\"", search1);
-        assertTrue(SearchCompiler.compile(search1).match(tag1));
-        assertFalse(SearchCompiler.compile(search1).match(tag2));
-        final String search2 = SearchCompiler.buildSearchStringForTag(tag1.getKey(), "");
-        assertEquals("\"foo=\"=*", search2);
-        assertTrue(SearchCompiler.compile(search2).match(tag1));
-        assertTrue(SearchCompiler.compile(search2).match(tag2));
-    }
-
-    /**
-     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13870">Bug #13870</a>.
-     * @throws ParseError always
-     */
-    @Test(expected = ParseError.class)
-    public void testPattern13870() throws ParseError {
-        // https://bugs.openjdk.java.net/browse/JI-9044959
-        SearchSetting setting = new SearchSetting();
-        setting.regexSearch = true;
-        setting.text = "[";
-        SearchCompiler.compile(setting);
-    }
-
-    /**
-     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/14217">Bug #14217</a>.
-     * @throws Exception never
-     */
-    @Test
-    public void testTicket14217() throws Exception {
-        assertNotNull(SearchCompiler.compile(new String(Files.readAllBytes(
-                Paths.get(TestUtils.getRegressionDataFile(14217, "filter.txt"))), StandardCharsets.UTF_8)));
-    }
-
-    /**
-     * Unit test of {@link SearchCompiler.ExactKeyValue.Mode} enum.
-     */
-    @Test
-    public void testEnumExactKeyValueMode() {
-        TestUtils.superficialEnumCodeCoverage(ExactKeyValue.Mode.class);
-    }
-
-    /**
-     * Robustness test for preset searching. Ensures that the query 'preset:' is not accepted.
-     * @throws ParseError always
-     * @since 12464
-     */
-    @Test(expected = ParseError.class)
-    public void testPresetSearchMissingValue() throws ParseError {
-        SearchSetting settings = new SearchSetting();
-        settings.text = "preset:";
-        settings.mapCSSSearch = false;
-
-        TaggingPresets.readFromPreferences();
-
-        SearchCompiler.compile(settings);
-    }
-
-    /**
-     * Robustness test for preset searching. Validates that it is not possible to search for
-     * non existing presets.
-     * @throws ParseError always
-     * @since 12464
-     */
-    @Test(expected = ParseError.class)
-    public void testPresetNotExist() throws ParseError {
-        String testPresetName = "groupnamethatshouldnotexist/namethatshouldnotexist";
-        SearchSetting settings = new SearchSetting();
-        settings.text = "preset:" + testPresetName;
-        settings.mapCSSSearch = false;
-
-        // load presets
-        TaggingPresets.readFromPreferences();
-
-        SearchCompiler.compile(settings);
-    }
-
-    /**
-     * Robustness tests for preset searching. Ensures that combined preset names (having more than
-     * 1 word) must be enclosed with " .
-     * @throws ParseError always
-     * @since 12464
-     */
-    @Test(expected = ParseError.class)
-    public void testPresetMultipleWords() throws ParseError {
-        TaggingPreset testPreset = new TaggingPreset();
-        testPreset.name = "Test Combined Preset Name";
-        testPreset.group = new TaggingPresetMenu();
-        testPreset.group.name = "TestGroupName";
-
-        String combinedPresetname = testPreset.getRawName();
-        SearchSetting settings = new SearchSetting();
-        settings.text = "preset:" + combinedPresetname;
-        settings.mapCSSSearch = false;
-
-        // load presets
-        TaggingPresets.readFromPreferences();
-
-        SearchCompiler.compile(settings);
-    }
-
-
-    /**
-     * Ensures that correct presets are stored in the {@link org.openstreetmap.josm.actions.search.SearchCompiler.Preset}
-     * class against which the osm primitives are tested.
-     * @throws ParseError if an error has been encountered while compiling
-     * @throws NoSuchFieldException if there is no field called 'presets'
-     * @throws IllegalAccessException if cannot access the field where all matching presets are stored
-     * @since 12464
-     */
-    @Test
-    public void testPresetLookup() throws ParseError, NoSuchFieldException, IllegalAccessException {
-        TaggingPreset testPreset = new TaggingPreset();
-        testPreset.name = "Test Preset Name";
-        testPreset.group = new TaggingPresetMenu();
-        testPreset.group.name = "Test Preset Group Name";
-
-        String query = "preset:" +
-                "\"" + testPreset.getRawName() + "\"";
-        SearchSetting settings = new SearchSetting();
-        settings.text = query;
-        settings.mapCSSSearch = false;
-
-        // load presets and add the test preset
-        TaggingPresets.readFromPreferences();
-        TaggingPresets.addTaggingPresets(Collections.singletonList(testPreset));
-
-        Match match = SearchCompiler.compile(settings);
-
-        // access the private field where all matching presets are stored
-        // and ensure that indeed the correct ones are there
-        Field field = match.getClass().getDeclaredField("presets");
-        field.setAccessible(true);
-        @SuppressWarnings("unchecked")
-        Collection<TaggingPreset> foundPresets = (Collection<TaggingPreset>) field.get(match);
-
-        assertEquals(1, foundPresets.size());
-        assertTrue(foundPresets.contains(testPreset));
-    }
-
-    /**
-     * Ensures that the wildcard search works and that correct presets are stored in
-     * the {@link org.openstreetmap.josm.actions.search.SearchCompiler.Preset} class against which
-     * the osm primitives are tested.
-     * @throws ParseError if an error has been encountered while compiling
-     * @throws NoSuchFieldException if there is no field called 'presets'
-     * @throws IllegalAccessException if cannot access the field where all matching presets are stored
-     * @since 12464
-     */
-    @Test
-    public void testPresetLookupWildcard() throws ParseError, NoSuchFieldException, IllegalAccessException {
-        TaggingPresetMenu group = new TaggingPresetMenu();
-        group.name = "TestPresetGroup";
-
-        TaggingPreset testPreset1 = new TaggingPreset();
-        testPreset1.name = "TestPreset1";
-        testPreset1.group = group;
-
-        TaggingPreset testPreset2 = new TaggingPreset();
-        testPreset2.name = "TestPreset2";
-        testPreset2.group = group;
-
-        TaggingPreset testPreset3 = new TaggingPreset();
-        testPreset3.name = "TestPreset3";
-        testPreset3.group = group;
-
-        String query = "preset:" + "\"" + group.getRawName() + "/*\"";
-        SearchSetting settings = new SearchSetting();
-        settings.text = query;
-        settings.mapCSSSearch = false;
-
-        TaggingPresets.readFromPreferences();
-        TaggingPresets.addTaggingPresets(Arrays.asList(testPreset1, testPreset2, testPreset3));
-
-        Match match = SearchCompiler.compile(settings);
-
-        // access the private field where all matching presets are stored
-        // and ensure that indeed the correct ones are there
-        Field field = match.getClass().getDeclaredField("presets");
-        field.setAccessible(true);
-        @SuppressWarnings("unchecked")
-        Collection<TaggingPreset> foundPresets = (Collection<TaggingPreset>) field.get(match);
-
-        assertEquals(3, foundPresets.size());
-        assertTrue(foundPresets.contains(testPreset1));
-        assertTrue(foundPresets.contains(testPreset2));
-        assertTrue(foundPresets.contains(testPreset3));
-    }
-
-    /**
-     * Ensures that correct primitives are matched against the specified preset.
-     * @throws ParseError if an error has been encountered while compiling
-     * @since 12464
-     */
-    @Test
-    public void testPreset() throws ParseError {
-        final String presetName = "Test Preset Name";
-        final String presetGroupName = "Test Preset Group";
-        final String key = "test_key1";
-        final String val = "test_val1";
-
-        Key key1 = new Key();
-        key1.key = key;
-        key1.value = val;
-
-        TaggingPreset testPreset = new TaggingPreset();
-        testPreset.name = presetName;
-        testPreset.types = Collections.singleton(TaggingPresetType.NODE);
-        testPreset.data.add(key1);
-        testPreset.group = new TaggingPresetMenu();
-        testPreset.group.name = presetGroupName;
-
-        TaggingPresets.readFromPreferences();
-        TaggingPresets.addTaggingPresets(Collections.singleton(testPreset));
-
-        String query = "preset:" + "\"" + testPreset.getRawName() + "\"";
-
-        SearchContext ctx = new SearchContext(query);
-        ctx.n1.put(key, val);
-        ctx.n2.put(key, val);
-
-        for (OsmPrimitive osm : new OsmPrimitive[] {ctx.n1, ctx.n2}) {
-            ctx.match(osm, true);
-        }
-
-        for (OsmPrimitive osm : new OsmPrimitive[] {ctx.r1, ctx.r2, ctx.w1, ctx.w2}) {
-            ctx.match(osm, false);
-        }
-    }
-}
-
Index: /trunk/test/unit/org/openstreetmap/josm/data/osm/FilterTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/data/osm/FilterTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/data/osm/FilterTest.java	(revision 12656)
@@ -17,7 +17,7 @@
 import org.junit.Test;
 import org.openstreetmap.josm.actions.search.SearchAction.SearchMode;
-import org.openstreetmap.josm.actions.search.SearchCompiler.ParseError;
 import org.openstreetmap.josm.data.coor.LatLon;
 import org.openstreetmap.josm.data.osm.Filter.FilterPreferenceEntry;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.gui.progress.NullProgressMonitor;
 import org.openstreetmap.josm.io.OsmReader;
@@ -41,5 +41,5 @@
 
     @Test
-    public void testBasic() throws ParseError {
+    public void testBasic() throws SearchParseError {
         DataSet ds = new DataSet();
         Node n1 = new Node(LatLon.ZERO);
Index: /trunk/test/unit/org/openstreetmap/josm/data/osm/search/PushbackTokenizerTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/data/osm/search/PushbackTokenizerTest.java	(revision 12656)
+++ /trunk/test/unit/org/openstreetmap/josm/data/osm/search/PushbackTokenizerTest.java	(revision 12656)
@@ -0,0 +1,31 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.search;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.data.osm.search.PushbackTokenizer.Token;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Unit tests for class {@link SearchCompiler}.
+ */
+public class PushbackTokenizerTest {
+
+    /**
+     * Setup rules.
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules();
+
+    /**
+     * Unit test of {@link Token} enum.
+     */
+    @Test
+    public void testEnumToken() {
+        TestUtils.superficialEnumCodeCoverage(Token.class);
+    }
+}
Index: /trunk/test/unit/org/openstreetmap/josm/data/osm/search/SearchCompilerTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/data/osm/search/SearchCompilerTest.java	(revision 12656)
+++ /trunk/test/unit/org/openstreetmap/josm/data/osm/search/SearchCompilerTest.java	(revision 12656)
@@ -0,0 +1,689 @@
+// License: GPL. For details, see LICENSE file.
+package org.openstreetmap.josm.data.osm.search;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.lang.reflect.Field;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.openstreetmap.josm.TestUtils;
+import org.openstreetmap.josm.actions.search.SearchAction.SearchSetting;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.DataSet;
+import org.openstreetmap.josm.data.osm.Node;
+import org.openstreetmap.josm.data.osm.OsmPrimitive;
+import org.openstreetmap.josm.data.osm.OsmPrimitiveType;
+import org.openstreetmap.josm.data.osm.Relation;
+import org.openstreetmap.josm.data.osm.RelationData;
+import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.User;
+import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.WayData;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.ExactKeyValue;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPreset;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetMenu;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresetType;
+import org.openstreetmap.josm.gui.tagging.presets.TaggingPresets;
+import org.openstreetmap.josm.gui.tagging.presets.items.Key;
+import org.openstreetmap.josm.testutils.JOSMTestRules;
+import org.openstreetmap.josm.tools.date.DateUtils;
+
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+
+/**
+ * Unit tests for class {@link SearchCompiler}.
+ */
+public class SearchCompilerTest {
+
+    /**
+     * We need prefs for this. We access preferences when creating OSM primitives.
+     */
+    @Rule
+    @SuppressFBWarnings(value = "URF_UNREAD_PUBLIC_OR_PROTECTED_FIELD")
+    public JOSMTestRules test = new JOSMTestRules().preferences();
+
+    private static final class SearchContext {
+        final DataSet ds = new DataSet();
+        final Node n1 = new Node(LatLon.ZERO);
+        final Node n2 = new Node(new LatLon(5, 5));
+        final Way w1 = new Way();
+        final Way w2 = new Way();
+        final Relation r1 = new Relation();
+        final Relation r2 = new Relation();
+
+        private final Match m;
+        private final Match n;
+
+        private SearchContext(String state) throws SearchParseError {
+            m = SearchCompiler.compile(state);
+            n = SearchCompiler.compile('-' + state);
+            ds.addPrimitive(n1);
+            ds.addPrimitive(n2);
+            w1.addNode(n1);
+            w1.addNode(n2);
+            w2.addNode(n1);
+            w2.addNode(n2);
+            ds.addPrimitive(w1);
+            ds.addPrimitive(w2);
+            r1.addMember(new RelationMember("", w1));
+            r1.addMember(new RelationMember("", w2));
+            r2.addMember(new RelationMember("", w1));
+            r2.addMember(new RelationMember("", w2));
+            ds.addPrimitive(r1);
+            ds.addPrimitive(r2);
+        }
+
+        private void match(OsmPrimitive p, boolean cond) {
+            if (cond) {
+                assertTrue(p.toString(), m.match(p));
+                assertFalse(p.toString(), n.match(p));
+            } else {
+                assertFalse(p.toString(), m.match(p));
+                assertTrue(p.toString(), n.match(p));
+            }
+        }
+    }
+
+    private static OsmPrimitive newPrimitive(String key, String value) {
+        final Node p = new Node();
+        p.put(key, value);
+        return p;
+    }
+
+    /**
+     * Search anything.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testAny() throws SearchParseError {
+        final SearchCompiler.Match c = SearchCompiler.compile("foo");
+        assertTrue(c.match(newPrimitive("foobar", "true")));
+        assertTrue(c.match(newPrimitive("name", "hello-foo-xy")));
+        assertFalse(c.match(newPrimitive("name", "X")));
+        assertEquals("foo", c.toString());
+    }
+
+    /**
+     * Search by equality key=value.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testEquals() throws SearchParseError {
+        final SearchCompiler.Match c = SearchCompiler.compile("foo=bar");
+        assertFalse(c.match(newPrimitive("foobar", "true")));
+        assertTrue(c.match(newPrimitive("foo", "bar")));
+        assertFalse(c.match(newPrimitive("fooX", "bar")));
+        assertFalse(c.match(newPrimitive("foo", "barX")));
+        assertEquals("foo=bar", c.toString());
+    }
+
+    /**
+     * Search by comparison.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testCompare() throws SearchParseError {
+        final SearchCompiler.Match c1 = SearchCompiler.compile("start_date>1950");
+        assertTrue(c1.match(newPrimitive("start_date", "1950-01-01")));
+        assertTrue(c1.match(newPrimitive("start_date", "1960")));
+        assertFalse(c1.match(newPrimitive("start_date", "1950")));
+        assertFalse(c1.match(newPrimitive("start_date", "1000")));
+        assertTrue(c1.match(newPrimitive("start_date", "101010")));
+
+        final SearchCompiler.Match c2 = SearchCompiler.compile("start_date<1960");
+        assertTrue(c2.match(newPrimitive("start_date", "1950-01-01")));
+        assertFalse(c2.match(newPrimitive("start_date", "1960")));
+        assertTrue(c2.match(newPrimitive("start_date", "1950")));
+        assertTrue(c2.match(newPrimitive("start_date", "1000")));
+        assertTrue(c2.match(newPrimitive("start_date", "200")));
+
+        final SearchCompiler.Match c3 = SearchCompiler.compile("name<I");
+        assertTrue(c3.match(newPrimitive("name", "Alpha")));
+        assertFalse(c3.match(newPrimitive("name", "Sigma")));
+
+        final SearchCompiler.Match c4 = SearchCompiler.compile("\"start_date\"<1960");
+        assertTrue(c4.match(newPrimitive("start_date", "1950-01-01")));
+        assertFalse(c4.match(newPrimitive("start_date", "2000")));
+
+        final SearchCompiler.Match c5 = SearchCompiler.compile("height>180");
+        assertTrue(c5.match(newPrimitive("height", "200")));
+        assertTrue(c5.match(newPrimitive("height", "99999")));
+        assertFalse(c5.match(newPrimitive("height", "50")));
+        assertFalse(c5.match(newPrimitive("height", "-9999")));
+        assertFalse(c5.match(newPrimitive("height", "fixme")));
+
+        final SearchCompiler.Match c6 = SearchCompiler.compile("name>C");
+        assertTrue(c6.match(newPrimitive("name", "Delta")));
+        assertFalse(c6.match(newPrimitive("name", "Alpha")));
+    }
+
+    /**
+     * Search by nth.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testNth() throws SearchParseError {
+        final DataSet dataSet = new DataSet();
+        final Way way = new Way();
+        final Node node0 = new Node(new LatLon(1, 1));
+        final Node node1 = new Node(new LatLon(2, 2));
+        final Node node2 = new Node(new LatLon(3, 3));
+        dataSet.addPrimitive(way);
+        dataSet.addPrimitive(node0);
+        dataSet.addPrimitive(node1);
+        dataSet.addPrimitive(node2);
+        way.addNode(node0);
+        way.addNode(node1);
+        way.addNode(node2);
+        assertFalse(SearchCompiler.compile("nth:2").match(node1));
+        assertTrue(SearchCompiler.compile("nth:1").match(node1));
+        assertFalse(SearchCompiler.compile("nth:0").match(node1));
+        assertTrue(SearchCompiler.compile("nth:0").match(node0));
+        assertTrue(SearchCompiler.compile("nth:2").match(node2));
+        assertTrue(SearchCompiler.compile("nth:-1").match(node2));
+        assertTrue(SearchCompiler.compile("nth:-2").match(node1));
+        assertTrue(SearchCompiler.compile("nth:-3").match(node0));
+    }
+
+    /**
+     * Search by negative nth.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testNthParseNegative() throws SearchParseError {
+        assertEquals("Nth{nth=-1, modulo=false}", SearchCompiler.compile("nth:-1").toString());
+    }
+
+    /**
+     * Search by modified status.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testModified() throws SearchParseError {
+        SearchContext sc = new SearchContext("modified");
+        // Not modified but new
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            assertFalse(p.toString(), p.isModified());
+            assertTrue(p.toString(), p.isNewOrUndeleted());
+            sc.match(p, true);
+        }
+        // Modified and new
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            p.setModified(true);
+            assertTrue(p.toString(), p.isModified());
+            assertTrue(p.toString(), p.isNewOrUndeleted());
+            sc.match(p, true);
+        }
+        // Modified but not new
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            p.setOsmId(1, 1);
+            assertTrue(p.toString(), p.isModified());
+            assertFalse(p.toString(), p.isNewOrUndeleted());
+            sc.match(p, true);
+        }
+        // Not modified nor new
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
+            p.setOsmId(2, 2);
+            assertFalse(p.toString(), p.isModified());
+            assertFalse(p.toString(), p.isNewOrUndeleted());
+            sc.match(p, false);
+        }
+    }
+
+    /**
+     * Search by selected status.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testSelected() throws SearchParseError {
+        SearchContext sc = new SearchContext("selected");
+        // Not selected
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            assertFalse(p.toString(), p.isSelected());
+            sc.match(p, false);
+        }
+        // Selected
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
+            sc.ds.addSelected(p);
+            assertTrue(p.toString(), p.isSelected());
+            sc.match(p, true);
+        }
+    }
+
+    /**
+     * Search by incomplete status.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testIncomplete() throws SearchParseError {
+        SearchContext sc = new SearchContext("incomplete");
+        // Not incomplete
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            assertFalse(p.toString(), p.isIncomplete());
+            sc.match(p, false);
+        }
+        // Incomplete
+        sc.n2.setCoor(null);
+        WayData wd = new WayData();
+        wd.setIncomplete(true);
+        sc.w2.load(wd);
+        RelationData rd = new RelationData();
+        rd.setIncomplete(true);
+        sc.r2.load(rd);
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
+            assertTrue(p.toString(), p.isIncomplete());
+            sc.match(p, true);
+        }
+    }
+
+    /**
+     * Search by untagged status.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testUntagged() throws SearchParseError {
+        SearchContext sc = new SearchContext("untagged");
+        // Untagged
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            assertFalse(p.toString(), p.isTagged());
+            sc.match(p, true);
+        }
+        // Tagged
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
+            p.put("foo", "bar");
+            assertTrue(p.toString(), p.isTagged());
+            sc.match(p, false);
+        }
+    }
+
+    /**
+     * Search by closed status.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testClosed() throws SearchParseError {
+        SearchContext sc = new SearchContext("closed");
+        // Closed
+        sc.w1.addNode(sc.n1);
+        for (Way w : new Way[]{sc.w1}) {
+            assertTrue(w.toString(), w.isClosed());
+            sc.match(w, true);
+        }
+        // Unclosed
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w2, sc.r1, sc.r2}) {
+            sc.match(p, false);
+        }
+    }
+
+    /**
+     * Search by new status.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testNew() throws SearchParseError {
+        SearchContext sc = new SearchContext("new");
+        // New
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.w1, sc.r1}) {
+            assertTrue(p.toString(), p.isNew());
+            sc.match(p, true);
+        }
+        // Not new
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n2, sc.w2, sc.r2}) {
+            p.setOsmId(2, 2);
+            assertFalse(p.toString(), p.isNew());
+            sc.match(p, false);
+        }
+    }
+
+    /**
+     * Search for node objects.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testTypeNode() throws SearchParseError {
+        final SearchContext sc = new SearchContext("type:node");
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
+            sc.match(p, OsmPrimitiveType.NODE.equals(p.getType()));
+        }
+    }
+
+    /**
+     * Search for way objects.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testTypeWay() throws SearchParseError {
+        final SearchContext sc = new SearchContext("type:way");
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
+            sc.match(p, OsmPrimitiveType.WAY.equals(p.getType()));
+        }
+    }
+
+    /**
+     * Search for relation objects.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testTypeRelation() throws SearchParseError {
+        final SearchContext sc = new SearchContext("type:relation");
+        for (OsmPrimitive p : new OsmPrimitive[]{sc.n1, sc.n2, sc.w1, sc.w2, sc.r1, sc.r2}) {
+            sc.match(p, OsmPrimitiveType.RELATION.equals(p.getType()));
+        }
+    }
+
+    /**
+     * Search for users.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testUser() throws SearchParseError {
+        final SearchContext foobar = new SearchContext("user:foobar");
+        foobar.n1.setUser(User.createLocalUser("foobar"));
+        foobar.match(foobar.n1, true);
+        foobar.match(foobar.n2, false);
+        final SearchContext anonymous = new SearchContext("user:anonymous");
+        anonymous.n1.setUser(User.createLocalUser("foobar"));
+        anonymous.match(anonymous.n1, false);
+        anonymous.match(anonymous.n2, true);
+    }
+
+    /**
+     * Compiles "foo type bar" and tests the parse error message
+     */
+    @Test
+    public void testFooTypeBar() {
+        try {
+            SearchCompiler.compile("foo type bar");
+            fail();
+        } catch (SearchParseError parseError) {
+            assertEquals("<html>Expecting <code>:</code> after <i>type</i>", parseError.getMessage());
+        }
+    }
+
+    /**
+     * Search for primitive timestamps.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testTimestamp() throws SearchParseError {
+        final Match search = SearchCompiler.compile("timestamp:2010/2011");
+        final Node n1 = new Node();
+        n1.setTimestamp(DateUtils.fromString("2010-01-22"));
+        assertTrue(search.match(n1));
+        n1.setTimestamp(DateUtils.fromString("2016-01-22"));
+        assertFalse(search.match(n1));
+    }
+
+    /**
+     * Tests the implementation of the Boolean logic.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testBooleanLogic() throws SearchParseError {
+        final SearchCompiler.Match c1 = SearchCompiler.compile("foo AND bar AND baz");
+        assertTrue(c1.match(newPrimitive("foobar", "baz")));
+        assertEquals("foo && bar && baz", c1.toString());
+        final SearchCompiler.Match c2 = SearchCompiler.compile("foo AND (bar OR baz)");
+        assertTrue(c2.match(newPrimitive("foobar", "yes")));
+        assertTrue(c2.match(newPrimitive("foobaz", "yes")));
+        assertEquals("foo && (bar || baz)", c2.toString());
+        final SearchCompiler.Match c3 = SearchCompiler.compile("foo OR (bar baz)");
+        assertEquals("foo || (bar && baz)", c3.toString());
+        final SearchCompiler.Match c4 = SearchCompiler.compile("foo1 OR (bar1 bar2 baz1 XOR baz2) OR foo2");
+        assertEquals("foo1 || (bar1 && bar2 && (baz1 ^ baz2)) || foo2", c4.toString());
+        final SearchCompiler.Match c5 = SearchCompiler.compile("foo1 XOR (baz1 XOR (bar baz))");
+        assertEquals("foo1 ^ baz1 ^ (bar && baz)", c5.toString());
+        final SearchCompiler.Match c6 = SearchCompiler.compile("foo1 XOR ((baz1 baz2) XOR (bar OR baz))");
+        assertEquals("foo1 ^ (baz1 && baz2) ^ (bar || baz)", c6.toString());
+    }
+
+    /**
+     * Tests {@code buildSearchStringForTag}.
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testBuildSearchStringForTag() throws SearchParseError {
+        final Tag tag1 = new Tag("foo=", "bar\"");
+        final Tag tag2 = new Tag("foo=", "=bar");
+        final String search1 = SearchCompiler.buildSearchStringForTag(tag1.getKey(), tag1.getValue());
+        assertEquals("\"foo=\"=\"bar\\\"\"", search1);
+        assertTrue(SearchCompiler.compile(search1).match(tag1));
+        assertFalse(SearchCompiler.compile(search1).match(tag2));
+        final String search2 = SearchCompiler.buildSearchStringForTag(tag1.getKey(), "");
+        assertEquals("\"foo=\"=*", search2);
+        assertTrue(SearchCompiler.compile(search2).match(tag1));
+        assertTrue(SearchCompiler.compile(search2).match(tag2));
+    }
+
+    /**
+     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/13870">Bug #13870</a>.
+     * @throws SearchParseError always
+     */
+    @Test(expected = SearchParseError.class)
+    public void testPattern13870() throws SearchParseError {
+        // https://bugs.openjdk.java.net/browse/JI-9044959
+        SearchSetting setting = new SearchSetting();
+        setting.regexSearch = true;
+        setting.text = "[";
+        SearchCompiler.compile(setting);
+    }
+
+    /**
+     * Non-regression test for <a href="https://josm.openstreetmap.de/ticket/14217">Bug #14217</a>.
+     * @throws Exception never
+     */
+    @Test
+    public void testTicket14217() throws Exception {
+        assertNotNull(SearchCompiler.compile(new String(Files.readAllBytes(
+                Paths.get(TestUtils.getRegressionDataFile(14217, "filter.txt"))), StandardCharsets.UTF_8)));
+    }
+
+    /**
+     * Unit test of {@link SearchCompiler.ExactKeyValue.Mode} enum.
+     */
+    @Test
+    public void testEnumExactKeyValueMode() {
+        TestUtils.superficialEnumCodeCoverage(ExactKeyValue.Mode.class);
+    }
+
+    /**
+     * Robustness test for preset searching. Ensures that the query 'preset:' is not accepted.
+     * @throws SearchParseError always
+     * @since 12464
+     */
+    @Test(expected = SearchParseError.class)
+    public void testPresetSearchMissingValue() throws SearchParseError {
+        SearchSetting settings = new SearchSetting();
+        settings.text = "preset:";
+        settings.mapCSSSearch = false;
+
+        TaggingPresets.readFromPreferences();
+
+        SearchCompiler.compile(settings);
+    }
+
+    /**
+     * Robustness test for preset searching. Validates that it is not possible to search for
+     * non existing presets.
+     * @throws SearchParseError always
+     * @since 12464
+     */
+    @Test(expected = SearchParseError.class)
+    public void testPresetNotExist() throws SearchParseError {
+        String testPresetName = "groupnamethatshouldnotexist/namethatshouldnotexist";
+        SearchSetting settings = new SearchSetting();
+        settings.text = "preset:" + testPresetName;
+        settings.mapCSSSearch = false;
+
+        // load presets
+        TaggingPresets.readFromPreferences();
+
+        SearchCompiler.compile(settings);
+    }
+
+    /**
+     * Robustness tests for preset searching. Ensures that combined preset names (having more than
+     * 1 word) must be enclosed with " .
+     * @throws SearchParseError always
+     * @since 12464
+     */
+    @Test(expected = SearchParseError.class)
+    public void testPresetMultipleWords() throws SearchParseError {
+        TaggingPreset testPreset = new TaggingPreset();
+        testPreset.name = "Test Combined Preset Name";
+        testPreset.group = new TaggingPresetMenu();
+        testPreset.group.name = "TestGroupName";
+
+        String combinedPresetname = testPreset.getRawName();
+        SearchSetting settings = new SearchSetting();
+        settings.text = "preset:" + combinedPresetname;
+        settings.mapCSSSearch = false;
+
+        // load presets
+        TaggingPresets.readFromPreferences();
+
+        SearchCompiler.compile(settings);
+    }
+
+
+    /**
+     * Ensures that correct presets are stored in the {@link org.openstreetmap.josm.data.osm.search.SearchCompiler.Preset}
+     * class against which the osm primitives are tested.
+     * @throws SearchParseError if an error has been encountered while compiling
+     * @throws NoSuchFieldException if there is no field called 'presets'
+     * @throws IllegalAccessException if cannot access the field where all matching presets are stored
+     * @since 12464
+     */
+    @Test
+    public void testPresetLookup() throws SearchParseError, NoSuchFieldException, IllegalAccessException {
+        TaggingPreset testPreset = new TaggingPreset();
+        testPreset.name = "Test Preset Name";
+        testPreset.group = new TaggingPresetMenu();
+        testPreset.group.name = "Test Preset Group Name";
+
+        String query = "preset:" +
+                "\"" + testPreset.getRawName() + "\"";
+        SearchSetting settings = new SearchSetting();
+        settings.text = query;
+        settings.mapCSSSearch = false;
+
+        // load presets and add the test preset
+        TaggingPresets.readFromPreferences();
+        TaggingPresets.addTaggingPresets(Collections.singletonList(testPreset));
+
+        Match match = SearchCompiler.compile(settings);
+
+        // access the private field where all matching presets are stored
+        // and ensure that indeed the correct ones are there
+        Field field = match.getClass().getDeclaredField("presets");
+        field.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        Collection<TaggingPreset> foundPresets = (Collection<TaggingPreset>) field.get(match);
+
+        assertEquals(1, foundPresets.size());
+        assertTrue(foundPresets.contains(testPreset));
+    }
+
+    /**
+     * Ensures that the wildcard search works and that correct presets are stored in
+     * the {@link org.openstreetmap.josm.data.osm.search.SearchCompiler.Preset} class against which
+     * the osm primitives are tested.
+     * @throws SearchParseError if an error has been encountered while compiling
+     * @throws NoSuchFieldException if there is no field called 'presets'
+     * @throws IllegalAccessException if cannot access the field where all matching presets are stored
+     * @since 12464
+     */
+    @Test
+    public void testPresetLookupWildcard() throws SearchParseError, NoSuchFieldException, IllegalAccessException {
+        TaggingPresetMenu group = new TaggingPresetMenu();
+        group.name = "TestPresetGroup";
+
+        TaggingPreset testPreset1 = new TaggingPreset();
+        testPreset1.name = "TestPreset1";
+        testPreset1.group = group;
+
+        TaggingPreset testPreset2 = new TaggingPreset();
+        testPreset2.name = "TestPreset2";
+        testPreset2.group = group;
+
+        TaggingPreset testPreset3 = new TaggingPreset();
+        testPreset3.name = "TestPreset3";
+        testPreset3.group = group;
+
+        String query = "preset:" + "\"" + group.getRawName() + "/*\"";
+        SearchSetting settings = new SearchSetting();
+        settings.text = query;
+        settings.mapCSSSearch = false;
+
+        TaggingPresets.readFromPreferences();
+        TaggingPresets.addTaggingPresets(Arrays.asList(testPreset1, testPreset2, testPreset3));
+
+        Match match = SearchCompiler.compile(settings);
+
+        // access the private field where all matching presets are stored
+        // and ensure that indeed the correct ones are there
+        Field field = match.getClass().getDeclaredField("presets");
+        field.setAccessible(true);
+        @SuppressWarnings("unchecked")
+        Collection<TaggingPreset> foundPresets = (Collection<TaggingPreset>) field.get(match);
+
+        assertEquals(3, foundPresets.size());
+        assertTrue(foundPresets.contains(testPreset1));
+        assertTrue(foundPresets.contains(testPreset2));
+        assertTrue(foundPresets.contains(testPreset3));
+    }
+
+    /**
+     * Ensures that correct primitives are matched against the specified preset.
+     * @throws SearchParseError if an error has been encountered while compiling
+     * @since 12464
+     */
+    @Test
+    public void testPreset() throws SearchParseError {
+        final String presetName = "Test Preset Name";
+        final String presetGroupName = "Test Preset Group";
+        final String key = "test_key1";
+        final String val = "test_val1";
+
+        Key key1 = new Key();
+        key1.key = key;
+        key1.value = val;
+
+        TaggingPreset testPreset = new TaggingPreset();
+        testPreset.name = presetName;
+        testPreset.types = Collections.singleton(TaggingPresetType.NODE);
+        testPreset.data.add(key1);
+        testPreset.group = new TaggingPresetMenu();
+        testPreset.group.name = presetGroupName;
+
+        TaggingPresets.readFromPreferences();
+        TaggingPresets.addTaggingPresets(Collections.singleton(testPreset));
+
+        String query = "preset:" + "\"" + testPreset.getRawName() + "\"";
+
+        SearchContext ctx = new SearchContext(query);
+        ctx.n1.put(key, val);
+        ctx.n2.put(key, val);
+
+        for (OsmPrimitive osm : new OsmPrimitive[] {ctx.n1, ctx.n2}) {
+            ctx.match(osm, true);
+        }
+
+        for (OsmPrimitive osm : new OsmPrimitive[] {ctx.r1, ctx.r2, ctx.w1, ctx.w2}) {
+            ctx.match(osm, false);
+        }
+    }
+}
Index: /trunk/test/unit/org/openstreetmap/josm/gui/dialogs/properties/RecentTagCollectionTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/gui/dialogs/properties/RecentTagCollectionTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/gui/dialogs/properties/RecentTagCollectionTest.java	(revision 12656)
@@ -12,6 +12,6 @@
 import org.junit.Test;
 import org.openstreetmap.josm.actions.search.SearchAction;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.osm.Tag;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.data.preferences.CollectionProperty;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
@@ -34,8 +34,8 @@
      * Performs various tests on a {@link RecentTagCollection}.
      *
-     * @throws SearchCompiler.ParseError if an error has been encountered while compiling
+     * @throws SearchParseError if an error has been encountered while compiling
      */
     @Test
-    public void testVarious() throws SearchCompiler.ParseError {
+    public void testVarious() throws SearchParseError {
         final RecentTagCollection recentTags = new RecentTagCollection(2);
         assertTrue(recentTags.isEmpty());
Index: /trunk/test/unit/org/openstreetmap/josm/tools/GeometryTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/GeometryTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/GeometryTest.java	(revision 12656)
@@ -9,9 +9,9 @@
 import org.junit.Test;
 import org.openstreetmap.josm.TestUtils;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
 import org.openstreetmap.josm.data.coor.EastNorth;
 import org.openstreetmap.josm.data.osm.DataSet;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.Way;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
 import org.openstreetmap.josm.io.OsmReader;
 import org.openstreetmap.josm.testutils.JOSMTestRules;
Index: /trunk/test/unit/org/openstreetmap/josm/tools/template_engine/TemplateParserTest.java
===================================================================
--- /trunk/test/unit/org/openstreetmap/josm/tools/template_engine/TemplateParserTest.java	(revision 12655)
+++ /trunk/test/unit/org/openstreetmap/josm/tools/template_engine/TemplateParserTest.java	(revision 12656)
@@ -9,9 +9,10 @@
 import org.junit.Test;
 import org.openstreetmap.josm.JOSMFixture;
-import org.openstreetmap.josm.actions.search.SearchCompiler;
-import org.openstreetmap.josm.actions.search.SearchCompiler.Match;
 import org.openstreetmap.josm.data.osm.Node;
 import org.openstreetmap.josm.data.osm.Relation;
 import org.openstreetmap.josm.data.osm.RelationMember;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler;
+import org.openstreetmap.josm.data.osm.search.SearchCompiler.Match;
+import org.openstreetmap.josm.data.osm.search.SearchParseError;
 import org.openstreetmap.josm.testutils.DatasetFactory;
 import org.unitils.reflectionassert.ReflectionAssert;
@@ -79,5 +80,5 @@
     }
 
-    private static Match compile(String expression) throws SearchCompiler.ParseError {
+    private static Match compile(String expression) throws SearchParseError {
         return SearchCompiler.compile(expression);
     }
@@ -86,8 +87,8 @@
      * Test to parse a search expression condition.
      * @throws ParseError if the template cannot be parsed
-     * @throws SearchCompiler.ParseError if an error has been encountered while compiling
-     */
-    @Test
-    public void testConditionSearchExpression() throws ParseError, SearchCompiler.ParseError {
+     * @throws SearchParseError if an error has been encountered while compiling
+     */
+    @Test
+    public void testConditionSearchExpression() throws ParseError, SearchParseError {
         TemplateParser parser = new TemplateParser("?{ admin_level = 2 'NUTS 1' | admin_level = 4 'NUTS 2' |  '{admin_level}'}");
         Condition condition = new Condition();
