Index: /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/WikipediaPlugin.java
===================================================================
--- /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/WikipediaPlugin.java	(revision 33620)
+++ /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/WikipediaPlugin.java	(revision 33621)
@@ -7,4 +7,6 @@
 import org.openstreetmap.josm.gui.MainMenu;
 import org.openstreetmap.josm.gui.MapFrame;
+import org.openstreetmap.josm.gui.download.DownloadDialog;
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
 import org.openstreetmap.josm.plugins.Plugin;
 import org.openstreetmap.josm.plugins.PluginInformation;
@@ -12,9 +14,9 @@
 import org.wikipedia.actions.WikipediaAddNamesAction;
 import org.wikipedia.actions.WikipediaCopyTemplate;
-import org.wikipedia.gui.WikidataItemSearchDialog;
-import org.wikipedia.gui.WikidataTagCellRenderer;
-import org.wikipedia.gui.WikipediaToggleDialog;
+import org.wikipedia.gui.*;
 
 public class WikipediaPlugin extends Plugin {
+
+    private PreferenceSetting preferences;
 
     public WikipediaPlugin(PluginInformation info) {
@@ -25,4 +27,6 @@
         MainMenu.add(dataMenu, new FetchWikidataAction());
         MainMenu.add(dataMenu, new WikidataItemSearchDialog.Action());
+
+        DownloadDialog.getInstance().addDownloadSource(new WikosmDownloadSource());
     }
 
@@ -34,3 +38,11 @@
         }
     }
+
+    @Override
+    public PreferenceSetting getPreferenceSetting() {
+        if (preferences == null) {
+            preferences = (new WikosmServerPreference.Factory()).createPreferenceSetting();
+        }
+        return preferences;
+    }
 }
Index: /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmDownloadSource.java
===================================================================
--- /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmDownloadSource.java	(revision 33621)
+++ /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmDownloadSource.java	(revision 33621)
@@ -0,0 +1,363 @@
+// License: GPL. For details, see LICENSE file.
+package org.wikipedia.gui;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.BorderLayout;
+import java.awt.Dimension;
+import java.awt.GridBagLayout;
+import java.awt.event.ActionEvent;
+import java.awt.event.FocusEvent;
+import java.awt.event.FocusListener;
+import java.util.Collection;
+import java.util.concurrent.Future;
+import java.util.function.Consumer;
+
+import javax.swing.*;
+import javax.swing.event.ListSelectionEvent;
+import javax.swing.event.ListSelectionListener;
+import javax.swing.plaf.basic.BasicArrowButton;
+
+import org.openstreetmap.josm.actions.downloadtasks.DownloadOsmTask;
+import org.openstreetmap.josm.actions.downloadtasks.PostDownloadHandler;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.preferences.AbstractProperty;
+import org.openstreetmap.josm.data.preferences.BooleanProperty;
+import org.openstreetmap.josm.data.preferences.IntegerProperty;
+import org.openstreetmap.josm.gui.MainApplication;
+import org.openstreetmap.josm.gui.download.AbstractDownloadSourcePanel;
+import org.openstreetmap.josm.gui.download.DownloadSettings;
+import org.openstreetmap.josm.gui.download.DownloadSource;
+import org.openstreetmap.josm.gui.download.DownloadSourceSizingPolicy;
+import org.openstreetmap.josm.gui.download.DownloadSourceSizingPolicy.AdjustableDownloadSizePolicy;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.gui.widgets.JosmTextArea;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.ImageProvider;
+import org.wikipedia.io.WikosmDownloadReader;
+
+/**
+ * Class defines the way data is fetched from Wikosm API.
+ */
+public class WikosmDownloadSource implements DownloadSource<WikosmDownloadSource.WikosmDownloadData> {
+
+    @Override
+    public AbstractDownloadSourcePanel<WikosmDownloadData> createPanel() {
+        return new WikosmDownloadSourcePanel(this);
+    }
+
+    @Override
+    public void doDownload(WikosmDownloadData data, DownloadSettings settings) {
+        Bounds area = settings.getDownloadBounds().orElse(new Bounds(0, 0, 0, 0));
+        DownloadOsmTask task = new DownloadOsmTask();
+        task.setZoomAfterDownload(settings.zoomToData());
+        Future<?> future = task.download(
+                new WikosmDownloadReader(area, WikosmDownloadReader.WIKOSM_SERVER.get(), data.getQuery(),
+                        settings.asNewLayer(), data.getDownloadReferrers(), data.getDownloadFull()),
+
+                settings.asNewLayer(), area, null);
+        MainApplication.worker.submit(new PostDownloadHandler(task, future, data.getErrorReporter()));
+    }
+
+    @Override
+    public String getLabel() {
+        return tr("Download from Wikosm API");
+    }
+
+    @Override
+    public boolean onlyExpert() {
+        return true;
+    }
+
+    /**
+     * The GUI representation of the Wikosm download source.
+     */
+    public static class WikosmDownloadSourcePanel extends AbstractDownloadSourcePanel<WikosmDownloadData> {
+
+        private static final String SIMPLE_NAME = "wikosmdownloadpanel";
+        private static final AbstractProperty<Integer> PANEL_SIZE_PROPERTY =
+                new IntegerProperty(TAB_SPLIT_NAMESPACE + SIMPLE_NAME, 150).cached();
+        private static final BooleanProperty WIKOSM_QUERY_LIST_OPENED =
+                new BooleanProperty("download.wikosm.query-list.opened", false);
+        private static final String ACTION_IMG_SUBDIR = "dialogs";
+
+        private final JosmTextArea wikosmQuery;
+        private final WikosmQueryList wikosmQueryList;
+        private final JCheckBox referrers;
+        private final JCheckBox fullRel;
+
+        /**
+         * Create a new {@link WikosmDownloadSourcePanel}
+         * @param ds The download source to create the panel for
+         */
+        public WikosmDownloadSourcePanel(WikosmDownloadSource ds) {
+            super(ds);
+            setLayout(new BorderLayout());
+
+            // CHECKSTYLE.OFF: LineLength
+            this.wikosmQuery = new JosmTextArea(
+                    "# " + tr("Enter your Wikosm SPARQL query below") + "\n"
+                            + "SELECT ?osmId ?loc WHERE { BIND(osmnode:2681940767 as ?osmId). ?osmId osmm:loc ?loc . }",
+                    8, 80);
+            // CHECKSTYLE.ON: LineLength
+            this.wikosmQuery.setFont(GuiHelper.getMonospacedFont(wikosmQuery));
+            this.wikosmQuery.addFocusListener(new FocusListener() {
+                @Override
+                public void focusGained(FocusEvent e) {
+                    wikosmQuery.selectAll();
+                }
+
+                @Override
+                public void focusLost(FocusEvent e) {
+                    // ignored
+                }
+            });
+
+
+            this.wikosmQueryList = new WikosmQueryList(this, this.wikosmQuery);
+            this.wikosmQueryList.setPreferredSize(new Dimension(350, 300));
+
+            EditSnippetAction edit = new EditSnippetAction();
+            RemoveSnippetAction remove = new RemoveSnippetAction();
+            this.wikosmQueryList.addSelectionListener(edit);
+            this.wikosmQueryList.addSelectionListener(remove);
+
+            JPanel listPanel = new JPanel(new GridBagLayout());
+            listPanel.add(new JLabel(tr("Your saved queries:")), GBC.eol().insets(2).anchor(GBC.CENTER));
+            listPanel.add(this.wikosmQueryList, GBC.eol().fill(GBC.BOTH));
+            listPanel.add(new JButton(new AddSnippetAction()), GBC.std().fill(GBC.HORIZONTAL));
+            listPanel.add(new JButton(edit), GBC.std().fill(GBC.HORIZONTAL));
+            listPanel.add(new JButton(remove), GBC.std().fill(GBC.HORIZONTAL));
+            listPanel.setVisible(WIKOSM_QUERY_LIST_OPENED.get());
+
+            JScrollPane scrollPane = new JScrollPane(wikosmQuery);
+            BasicArrowButton arrowButton = new BasicArrowButton(listPanel.isVisible()
+                    ? BasicArrowButton.EAST
+                    : BasicArrowButton.WEST);
+            arrowButton.setToolTipText(tr("Show/hide Wikosm snippet list"));
+            arrowButton.addActionListener(e -> {
+                if (listPanel.isVisible()) {
+                    listPanel.setVisible(false);
+                    arrowButton.setDirection(BasicArrowButton.WEST);
+                    WIKOSM_QUERY_LIST_OPENED.put(Boolean.FALSE);
+                } else {
+                    listPanel.setVisible(true);
+                    arrowButton.setDirection(BasicArrowButton.EAST);
+                    WIKOSM_QUERY_LIST_OPENED.put(Boolean.TRUE);
+                }
+            });
+
+
+            referrers = new JCheckBox(tr("Download referrers (parent relations)"));
+            referrers.setToolTipText(tr("Select if the referrers of the object should be downloaded as well, i.e.,"
+                    + "parent relations and for nodes, additionally, parent ways"));
+            referrers.setSelected(Config.getPref().getBoolean("wikosm.downloadprimitive.referrers", true));
+            referrers.addActionListener(e -> Config.getPref().putBoolean("wikosm.downloadprimitive.referrers", referrers.isSelected()));
+
+            fullRel = new JCheckBox(tr("Download relation members"));
+            fullRel.setToolTipText(tr("Select if the members of a relation should be downloaded as well"));
+            fullRel.setSelected(Config.getPref().getBoolean("wikosm.downloadprimitive.full", true));
+            fullRel.addActionListener(e -> Config.getPref().putBoolean("wikosm.downloadprimitive.full", fullRel.isSelected()));
+
+            JPanel centerPanel = new JPanel(new GridBagLayout());
+            centerPanel.add(scrollPane, GBC.eol().fill(GBC.BOTH));
+            centerPanel.add(referrers, GBC.std().anchor(GBC.WEST).insets(5, 5, 5, 5));
+            centerPanel.add(fullRel, GBC.std().anchor(GBC.WEST).insets(15, 5, 5, 5));
+
+
+            JPanel innerPanel = new JPanel(new BorderLayout());
+            innerPanel.add(centerPanel, BorderLayout.CENTER);
+            innerPanel.add(arrowButton, BorderLayout.EAST);
+
+            add(innerPanel, BorderLayout.CENTER);
+            add(listPanel, BorderLayout.EAST);
+
+            setMinimumSize(new Dimension(450, 240));
+        }
+
+        @Override
+        public WikosmDownloadData getData() {
+            String query = wikosmQuery.getText();
+            /*
+             * A callback that is passed to PostDownloadReporter that is called once the download task
+             * has finished. According to the number of errors happened, their type we decide whether we
+             * want to save the last query in WikosmQueryList.
+             */
+            Consumer<Collection<Object>> errorReporter = errors -> {
+
+                boolean onlyNoDataError = errors.size() == 1 &&
+                        errors.contains("No data found in this area.");
+
+                if (errors.isEmpty() || onlyNoDataError) {
+                    wikosmQueryList.saveHistoricItem(query);
+                }
+            };
+
+            return new WikosmDownloadData(query, referrers.isSelected(), fullRel.isSelected(), errorReporter);
+        }
+
+        @Override
+        public void rememberSettings() {
+            // nothing
+        }
+
+        @Override
+        public void restoreSettings() {
+            // nothing
+        }
+
+        @Override
+        public boolean checkDownload(DownloadSettings settings) {
+            String query = getData().getQuery();
+
+            /*
+             * Absence of the selected area can be justified only if the Wikosm query
+             * is not restricted to bbox.
+             */
+            if (!settings.getDownloadBounds().isPresent() && query.contains("{{bbox}}")) {
+                JOptionPane.showMessageDialog(
+                        this.getParent(),
+                        tr("Please select a download area first."),
+                        tr("Error"),
+                        JOptionPane.ERROR_MESSAGE
+                );
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public Icon getIcon() {
+            return ImageProvider.get(ACTION_IMG_SUBDIR, "wikosm");
+        }
+
+        @Override
+        public String getSimpleName() {
+            return SIMPLE_NAME;
+        }
+
+        @Override
+        public DownloadSourceSizingPolicy getSizingPolicy() {
+            return new AdjustableDownloadSizePolicy(PANEL_SIZE_PROPERTY);
+        }
+
+        /**
+         * Action that delegates snippet creation to {@link WikosmQueryList#createNewItem()}.
+         */
+        private class AddSnippetAction extends AbstractAction {
+
+            /**
+             * Constructs a new {@code AddSnippetAction}.
+             */
+            AddSnippetAction() {
+                super();
+                putValue(SMALL_ICON, ImageProvider.get(ACTION_IMG_SUBDIR, "add"));
+                putValue(SHORT_DESCRIPTION, tr("Add new snippet"));
+            }
+
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                wikosmQueryList.createNewItem();
+            }
+        }
+
+        /**
+         * Action that delegates snippet removal to {@link WikosmQueryList#removeSelectedItem()}.
+         */
+        private class RemoveSnippetAction extends AbstractAction implements ListSelectionListener {
+
+            /**
+             * Constructs a new {@code RemoveSnippetAction}.
+             */
+            RemoveSnippetAction() {
+                super();
+                putValue(SMALL_ICON, ImageProvider.get(ACTION_IMG_SUBDIR, "delete"));
+                putValue(SHORT_DESCRIPTION, tr("Delete selected snippet"));
+                checkEnabled();
+            }
+
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                wikosmQueryList.removeSelectedItem();
+            }
+
+            /**
+             * Disables the action if no items are selected.
+             */
+            void checkEnabled() {
+                setEnabled(wikosmQueryList.getSelectedItem().isPresent());
+            }
+
+            @Override
+            public void valueChanged(ListSelectionEvent e) {
+                checkEnabled();
+            }
+        }
+
+        /**
+         * Action that delegates snippet edit to {@link WikosmQueryList#editSelectedItem()}.
+         */
+        private class EditSnippetAction extends AbstractAction implements ListSelectionListener {
+
+            /**
+             * Constructs a new {@code EditSnippetAction}.
+             */
+            EditSnippetAction() {
+                super();
+                putValue(SMALL_ICON, ImageProvider.get(ACTION_IMG_SUBDIR, "edit"));
+                putValue(SHORT_DESCRIPTION, tr("Edit selected snippet"));
+                checkEnabled();
+            }
+
+            @Override
+            public void actionPerformed(ActionEvent e) {
+                wikosmQueryList.editSelectedItem();
+            }
+
+            /**
+             * Disables the action if no items are selected.
+             */
+            void checkEnabled() {
+                setEnabled(wikosmQueryList.getSelectedItem().isPresent());
+            }
+
+            @Override
+            public void valueChanged(ListSelectionEvent e) {
+                checkEnabled();
+            }
+        }
+    }
+
+    /**
+     * Encapsulates data that is required to preform download from Wikosm API.
+     */
+    static class WikosmDownloadData {
+        private final String query;
+        private final boolean downloadReferrers;
+        private final boolean downloadFull;
+        private final Consumer<Collection<Object>> errorReporter;
+
+        WikosmDownloadData(String query, boolean downloadReferrers, boolean downloadFull, Consumer<Collection<Object>> errorReporter) {
+            this.query = query;
+            this.downloadReferrers = downloadReferrers;
+            this.downloadFull = downloadFull;
+            this.errorReporter = errorReporter;
+        }
+
+        String getQuery() {
+            return this.query;
+        }
+
+        boolean getDownloadReferrers() {
+            return this.downloadReferrers;
+        }
+
+        boolean getDownloadFull() { return this.downloadFull; }
+
+        Consumer<Collection<Object>> getErrorReporter() {
+            return this.errorReporter;
+        }
+    }
+}
Index: /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmQueryList.java
===================================================================
--- /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmQueryList.java	(revision 33621)
+++ /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmQueryList.java	(revision 33621)
@@ -0,0 +1,656 @@
+// License: GPL. For details, see LICENSE file.
+package org.wikipedia.gui;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.Color;
+import java.awt.Component;
+import java.awt.Dimension;
+import java.awt.Font;
+import java.awt.GridBagLayout;
+import java.awt.Point;
+import java.awt.event.ActionEvent;
+import java.awt.event.ActionListener;
+import java.awt.event.MouseAdapter;
+import java.awt.event.MouseEvent;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+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.Objects;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+import javax.swing.AbstractAction;
+import javax.swing.BorderFactory;
+import javax.swing.JLabel;
+import javax.swing.JList;
+import javax.swing.JOptionPane;
+import javax.swing.JPanel;
+import javax.swing.JPopupMenu;
+import javax.swing.JScrollPane;
+import javax.swing.JTextField;
+import javax.swing.ListCellRenderer;
+import javax.swing.SwingUtilities;
+import javax.swing.border.CompoundBorder;
+import javax.swing.text.JTextComponent;
+
+import org.openstreetmap.josm.Main;
+import org.openstreetmap.josm.gui.ExtendedDialog;
+import org.openstreetmap.josm.gui.util.GuiHelper;
+import org.openstreetmap.josm.gui.widgets.AbstractTextComponentValidator;
+import org.openstreetmap.josm.gui.widgets.DefaultTextComponentValidator;
+import org.openstreetmap.josm.gui.widgets.JosmTextArea;
+import org.openstreetmap.josm.gui.widgets.SearchTextResultListPanel;
+import org.openstreetmap.josm.spi.preferences.Config;
+import org.openstreetmap.josm.tools.GBC;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
+
+/**
+ * A component to select user saved Wikosm queries.
+ */
+public final class WikosmQueryList extends SearchTextResultListPanel<WikosmQueryList.SelectorItem> {
+
+    private static final DateTimeFormatter FORMAT = DateTimeFormatter.ofPattern("HH:mm:ss, dd-MM-yyyy");
+
+    /*
+     * GUI elements
+     */
+    private final JTextComponent target;
+    private final Component componentParent;
+
+    /*
+     * All loaded elements within the list.
+     */
+    private final transient Map<String, SelectorItem> items;
+
+    /*
+     * Preferences
+     */
+    private static final String KEY_KEY = "key";
+    private static final String QUERY_KEY = "query";
+    private static final String LAST_EDIT_KEY = "lastEdit";
+    private static final String PREFERENCE_ITEMS = "download.wikosm.query";
+
+    private static final String TRANSLATED_HISTORY = tr("history");
+
+    /**
+     * Constructs a new {@code WikosmQueryList}.
+     * @param parent The parent of this component.
+     * @param target The text component to which the queries must be added.
+     */
+    public WikosmQueryList(Component parent, JTextComponent target) {
+        this.target = target;
+        this.componentParent = parent;
+        this.items = restorePreferences();
+
+        WikosmQueryListMouseAdapter mouseHandler = new WikosmQueryListMouseAdapter(lsResult, lsResultModel);
+        super.lsResult.setCellRenderer(new WikosmQueryCellRendered());
+        super.setDblClickListener(e -> doubleClickEvent());
+        super.lsResult.addMouseListener(mouseHandler);
+        super.lsResult.addMouseMotionListener(mouseHandler);
+
+        filterItems();
+    }
+
+    /**
+     * Returns currently selected element from the list.
+     * @return An {@link Optional#empty()} if nothing is selected, otherwise
+     * the idem is returned.
+     */
+    public synchronized Optional<SelectorItem> getSelectedItem() {
+        int idx = lsResult.getSelectedIndex();
+        if (lsResultModel.getSize() <= idx || idx == -1) {
+            return Optional.empty();
+        }
+
+        SelectorItem item = lsResultModel.getElementAt(idx);
+
+        filterItems();
+
+        return Optional.of(item);
+    }
+
+    /**
+     * Adds a new historic item to the list. The key has form 'history {current date}'.
+     * Note, the item is not saved if there is already a historic item with the same query.
+     * @param query The query of the item.
+     * @exception IllegalArgumentException if the query is empty.
+     * @exception NullPointerException if the query is {@code null}.
+     */
+    public synchronized void saveHistoricItem(String query) {
+        boolean historicExist = this.items.values().stream()
+                .map(SelectorItem::getQuery)
+                .anyMatch(q -> q.equals(query));
+
+        if (!historicExist) {
+            SelectorItem item = new SelectorItem(
+                    TRANSLATED_HISTORY + " " + LocalDateTime.now().format(FORMAT), query);
+
+            this.items.put(item.getKey(), item);
+
+            savePreferences();
+            filterItems();
+        }
+    }
+
+    /**
+     * Removes currently selected item, saves the current state to preferences and
+     * updates the view.
+     */
+    public synchronized void removeSelectedItem() {
+        Optional<SelectorItem> it = this.getSelectedItem();
+
+        if (!it.isPresent()) {
+            JOptionPane.showMessageDialog(
+                    componentParent,
+                    tr("Please select an item first"));
+            return;
+        }
+
+        SelectorItem item = it.get();
+        if (this.items.remove(item.getKey(), item)) {
+            clearSelection();
+            savePreferences();
+            filterItems();
+        }
+    }
+
+    /**
+     * Opens {@link EditItemDialog} for the selected item, saves the current state
+     * to preferences and updates the view.
+     */
+    public synchronized void editSelectedItem() {
+        Optional<SelectorItem> it = this.getSelectedItem();
+
+        if (!it.isPresent()) {
+            JOptionPane.showMessageDialog(
+                    componentParent,
+                    tr("Please select an item first"));
+            return;
+        }
+
+        SelectorItem item = it.get();
+
+        EditItemDialog dialog = new EditItemDialog(
+                componentParent,
+                tr("Edit item"),
+                item,
+                tr("Save"), tr("Cancel"));
+        dialog.showDialog();
+
+        Optional<SelectorItem> editedItem = dialog.getOutputItem();
+        editedItem.ifPresent(i -> {
+            this.items.remove(item.getKey(), item);
+            this.items.put(i.getKey(), i);
+
+            savePreferences();
+            filterItems();
+        });
+    }
+
+    /**
+     * Opens {@link EditItemDialog}, saves the state to preferences if a new item is added
+     * and updates the view.
+     */
+    public synchronized void createNewItem() {
+        EditItemDialog dialog = new EditItemDialog(componentParent, tr("Add snippet"), tr("Add"));
+        dialog.showDialog();
+
+        Optional<SelectorItem> newItem = dialog.getOutputItem();
+        newItem.ifPresent(i -> {
+            items.put(i.getKey(), i);
+            savePreferences();
+            filterItems();
+        });
+    }
+
+    @Override
+    public void setDblClickListener(ActionListener dblClickListener) {
+        // this listener is already set within this class
+    }
+
+    @Override
+    protected void filterItems() {
+        String text = edSearchText.getText().toLowerCase(Locale.ENGLISH);
+        List<SelectorItem> matchingItems = this.items.values().stream()
+                .sorted((i1, i2) -> i2.getLastEdit().compareTo(i1.getLastEdit()))
+                .filter(item -> item.getKey().contains(text))
+                .collect(Collectors.toList());
+
+        super.lsResultModel.setItems(matchingItems);
+    }
+
+    private void doubleClickEvent() {
+        Optional<SelectorItem> selectedItem = this.getSelectedItem();
+
+        if (!selectedItem.isPresent()) {
+            return;
+        }
+
+        SelectorItem item = selectedItem.get();
+        this.target.setText(item.getQuery());
+    }
+
+    /**
+     * Saves all elements from the list to {@link Main#pref}.
+     */
+    private void savePreferences() {
+        List<Map<String, String>> toSave = new ArrayList<>(this.items.size());
+        for (SelectorItem item : this.items.values()) {
+            Map<String, String> it = new HashMap<>();
+            it.put(KEY_KEY, item.getKey());
+            it.put(QUERY_KEY, item.getQuery());
+            it.put(LAST_EDIT_KEY, item.getLastEdit().format(FORMAT));
+
+            toSave.add(it);
+        }
+
+        Config.getPref().putListOfMaps(PREFERENCE_ITEMS, toSave);
+    }
+
+    /**
+     * Loads the user saved items from {@link Main#pref}.
+     * @return A set of the user saved items.
+     */
+    private static Map<String, SelectorItem> restorePreferences() {
+        Collection<Map<String, String>> toRetrieve =
+                Config.getPref().getListOfMaps(PREFERENCE_ITEMS, Collections.emptyList());
+        Map<String, SelectorItem> result = new HashMap<>();
+
+        for (Map<String, String> entry : toRetrieve) {
+            try {
+                String key = entry.get(KEY_KEY);
+                String query = entry.get(QUERY_KEY);
+                String lastEditText = entry.get(LAST_EDIT_KEY);
+                // Compatibility: Some entries may not have a last edit set.
+                LocalDateTime lastEdit = lastEditText == null ? LocalDateTime.MIN : LocalDateTime.parse(lastEditText, FORMAT);
+
+                result.put(key, new SelectorItem(key, query, lastEdit));
+            } catch (IllegalArgumentException | DateTimeParseException e) {
+                // skip any corrupted item
+                Logging.error(e);
+            }
+        }
+
+        return result;
+    }
+
+    private class WikosmQueryListMouseAdapter extends MouseAdapter {
+
+        private final JList<SelectorItem> list;
+        private final ResultListModel<SelectorItem> model;
+        private final JPopupMenu emptySelectionPopup = new JPopupMenu();
+        private final JPopupMenu elementPopup = new JPopupMenu();
+
+        WikosmQueryListMouseAdapter(JList<SelectorItem> list, ResultListModel<SelectorItem> listModel) {
+            this.list = list;
+            this.model = listModel;
+
+            this.initPopupMenus();
+        }
+
+        /*
+         * Do not select the closest element if the user clicked on
+         * an empty area within the list.
+         */
+        private int locationToIndex(Point p) {
+            int idx = list.locationToIndex(p);
+
+            if (idx != -1 && !list.getCellBounds(idx, idx).contains(p)) {
+                return -1;
+            } else {
+                return idx;
+            }
+        }
+
+        @Override
+        public void mouseClicked(MouseEvent e) {
+            super.mouseClicked(e);
+            if (SwingUtilities.isRightMouseButton(e)) {
+                int index = locationToIndex(e.getPoint());
+
+                if (model.getSize() == 0 || index == -1) {
+                    list.clearSelection();
+                    emptySelectionPopup.show(list, e.getX(), e.getY());
+                } else {
+                    list.setSelectedIndex(index);
+                    list.ensureIndexIsVisible(index);
+                    elementPopup.show(list, e.getX(), e.getY());
+                }
+            }
+        }
+
+        @Override
+        public void mouseMoved(MouseEvent e) {
+            super.mouseMoved(e);
+            int idx = locationToIndex(e.getPoint());
+            if (idx == -1) {
+                return;
+            }
+
+            SelectorItem item = model.getElementAt(idx);
+            list.setToolTipText("<html><pre style='width:300px;'>" +
+                    Utils.escapeReservedCharactersHTML(Utils.restrictStringLines(item.getQuery(), 9)));
+        }
+
+        private void initPopupMenus() {
+            AbstractAction add = new AbstractAction(tr("Add")) {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    createNewItem();
+                }
+            };
+            AbstractAction edit = new AbstractAction(tr("Edit")) {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    editSelectedItem();
+                }
+            };
+            AbstractAction remove = new AbstractAction(tr("Remove")) {
+                @Override
+                public void actionPerformed(ActionEvent e) {
+                    removeSelectedItem();
+                }
+            };
+            this.emptySelectionPopup.add(add);
+            this.elementPopup.add(add);
+            this.elementPopup.add(edit);
+            this.elementPopup.add(remove);
+        }
+    }
+
+    /**
+     * This class defines the way each element is rendered in the list.
+     */
+    private static class WikosmQueryCellRendered extends JLabel implements ListCellRenderer<SelectorItem> {
+
+        WikosmQueryCellRendered() {
+            setOpaque(true);
+        }
+
+        @Override
+        public Component getListCellRendererComponent(
+                JList<? extends SelectorItem> list,
+                SelectorItem value,
+                int index,
+                boolean isSelected,
+                boolean cellHasFocus) {
+
+            Font font = list.getFont();
+            if (isSelected) {
+                setFont(new Font(font.getFontName(), Font.BOLD, font.getSize() + 2));
+                setBackground(list.getSelectionBackground());
+                setForeground(list.getSelectionForeground());
+            } else {
+                setFont(new Font(font.getFontName(), Font.PLAIN, font.getSize() + 2));
+                setBackground(list.getBackground());
+                setForeground(list.getForeground());
+            }
+
+            setEnabled(list.isEnabled());
+            setText(value.getKey());
+
+            if (isSelected && cellHasFocus) {
+                setBorder(new CompoundBorder(
+                        BorderFactory.createLineBorder(Color.BLACK, 1),
+                        BorderFactory.createEmptyBorder(2, 0, 2, 0)));
+            } else {
+                setBorder(new CompoundBorder(
+                        null,
+                        BorderFactory.createEmptyBorder(2, 0, 2, 0)));
+            }
+
+            return this;
+        }
+    }
+
+    /**
+     * Dialog that provides functionality to add/edit an item from the list.
+     */
+    private final class EditItemDialog extends ExtendedDialog {
+
+        private final JTextField name;
+        private final JosmTextArea query;
+
+        private final transient AbstractTextComponentValidator queryValidator;
+        private final transient AbstractTextComponentValidator nameValidator;
+
+        private static final int SUCCESS_BTN = 0;
+        private static final int CANCEL_BTN = 1;
+
+        private final transient SelectorItem itemToEdit;
+
+        /**
+         * Added/Edited object to be returned. If {@link Optional#empty()} then probably
+         * the user closed the dialog, otherwise {@link SelectorItem} is present.
+         */
+        private transient Optional<SelectorItem> outputItem = Optional.empty();
+
+        EditItemDialog(Component parent, String title, String... buttonTexts) {
+            this(parent, title, null, buttonTexts);
+        }
+
+        EditItemDialog(
+                Component parent,
+                String title,
+                SelectorItem itemToEdit,
+                String... buttonTexts) {
+            super(parent, title, buttonTexts);
+
+            this.itemToEdit = itemToEdit;
+
+            String nameToEdit = itemToEdit == null ? "" : itemToEdit.getKey();
+            String queryToEdit = itemToEdit == null ? "" : itemToEdit.getQuery();
+
+            this.name = new JTextField(nameToEdit);
+            this.query = new JosmTextArea(queryToEdit);
+
+            this.queryValidator = new DefaultTextComponentValidator(this.query, "", tr("Query cannot be empty"));
+            this.nameValidator = new AbstractTextComponentValidator(this.name) {
+                @Override
+                public void validate() {
+                    if (isValid()) {
+                        feedbackValid(tr("This name can be used for the item"));
+                    } else {
+                        feedbackInvalid(tr("Item with this name already exists"));
+                    }
+                }
+
+                @Override
+                public boolean isValid() {
+                    String currentName = name.getText();
+
+                    boolean notEmpty = !Utils.isStripEmpty(currentName);
+                    boolean exist = !currentName.equals(nameToEdit) &&
+                                        items.containsKey(currentName);
+
+                    return notEmpty && !exist;
+                }
+            };
+
+            this.name.getDocument().addDocumentListener(this.nameValidator);
+            this.query.getDocument().addDocumentListener(this.queryValidator);
+
+            JPanel panel = new JPanel(new GridBagLayout());
+            JScrollPane queryScrollPane = GuiHelper.embedInVerticalScrollPane(this.query);
+            queryScrollPane.getVerticalScrollBar().setUnitIncrement(10); // make scrolling smooth
+
+            GBC constraint = GBC.eol().insets(8, 0, 8, 8).anchor(GBC.CENTER).fill(GBC.HORIZONTAL);
+            constraint.ipady = 250;
+            panel.add(this.name, GBC.eol().insets(5).anchor(GBC.SOUTHEAST).fill(GBC.HORIZONTAL));
+            panel.add(queryScrollPane, constraint);
+
+            setDefaultButton(SUCCESS_BTN + 1);
+            setCancelButton(CANCEL_BTN + 1);
+            setPreferredSize(new Dimension(400, 400));
+            setContent(panel, false);
+        }
+
+        /**
+         * Gets a new {@link SelectorItem} if one was created/modified.
+         * @return A {@link SelectorItem} object created out of the fields of the dialog.
+         */
+        public Optional<SelectorItem> getOutputItem() {
+            return this.outputItem;
+        }
+
+        @Override
+        protected void buttonAction(int buttonIndex, ActionEvent evt) {
+            if (buttonIndex == SUCCESS_BTN) {
+                if (!this.nameValidator.isValid()) {
+                    JOptionPane.showMessageDialog(
+                            componentParent,
+                            tr("The item cannot be created with provided name"),
+                            tr("Warning"),
+                            JOptionPane.WARNING_MESSAGE);
+
+                    return;
+                } else if (!this.queryValidator.isValid()) {
+                    JOptionPane.showMessageDialog(
+                            componentParent,
+                            tr("The item cannot be created with an empty query"),
+                            tr("Warning"),
+                            JOptionPane.WARNING_MESSAGE);
+
+                    return;
+                } else if (this.itemToEdit != null) { // editing the item
+                    String newKey = this.name.getText();
+                    String newQuery = this.query.getText();
+
+                    String itemKey = this.itemToEdit.getKey();
+                    String itemQuery = this.itemToEdit.getQuery();
+
+                    this.outputItem = Optional.of(new SelectorItem(
+                            this.name.getText(),
+                            this.query.getText(),
+                            !newKey.equals(itemKey) || !newQuery.equals(itemQuery)
+                                ? LocalDateTime.now()
+                                : this.itemToEdit.getLastEdit()));
+
+                } else { // creating new
+                    this.outputItem = Optional.of(new SelectorItem(
+                            this.name.getText(),
+                            this.query.getText()));
+                }
+            }
+
+            super.buttonAction(buttonIndex, evt);
+        }
+    }
+
+    /**
+     * This class represents an Wikosm query used by the user that can be
+     * shown within {@link WikosmQueryList}.
+     */
+    public static class SelectorItem {
+        private final String itemKey;
+        private final String query;
+        private final LocalDateTime lastEdit;
+
+        /**
+         * Constructs a new {@code SelectorItem}.
+         * @param key The key of this item.
+         * @param query The query of the item.
+         * @exception NullPointerException if any parameter is {@code null}.
+         * @exception IllegalArgumentException if any parameter is empty.
+         */
+        public SelectorItem(String key, String query) {
+            this(key, query, LocalDateTime.now());
+        }
+
+        /**
+         * Constructs a new {@code SelectorItem}.
+         * @param key The key of this item.
+         * @param query The query of the item.
+         * @param lastEdit The latest when the item was
+         * @exception NullPointerException if any parameter is {@code null}.
+         * @exception IllegalArgumentException if any parameter is empty.
+         */
+        public SelectorItem(String key, String query, LocalDateTime lastEdit) {
+            Objects.requireNonNull(key, "The name of the item cannot be null");
+            Objects.requireNonNull(query, "The query of the item cannot be null");
+            Objects.requireNonNull(lastEdit, "The last edit date time cannot be null");
+
+            if (Utils.isStripEmpty(key)) {
+                throw new IllegalArgumentException("The key of the item cannot be empty");
+            }
+            if (Utils.isStripEmpty(query)) {
+                throw new IllegalArgumentException("The query cannot be empty");
+            }
+
+            this.itemKey = key;
+            this.query = query;
+            this.lastEdit = lastEdit;
+        }
+
+        /**
+         * Gets the key (a string that is displayed in the selector) of this item.
+         * @return A string representing the key of this item.
+         */
+        public String getKey() {
+            return this.itemKey;
+        }
+
+        /**
+         * Gets the Wikosm query of this item.
+         * @return A string representing the Wikosm query of this item.
+         */
+        public String getQuery() {
+            return this.query;
+        }
+
+        /**
+         * Gets the latest date time when the item was created/changed.
+         * @return The latest date time when the item was created/changed.
+         */
+        public LocalDateTime getLastEdit() {
+            return lastEdit;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((itemKey == null) ? 0 : itemKey.hashCode());
+            result = prime * result + ((query == null) ? 0 : query.hashCode());
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            SelectorItem other = (SelectorItem) obj;
+            if (itemKey == null) {
+                if (other.itemKey != null) {
+                    return false;
+                }
+            } else if (!itemKey.equals(other.itemKey)) {
+                return false;
+            }
+            if (query == null) {
+                if (other.query != null) {
+                    return false;
+                }
+            } else if (!query.equals(other.query)) {
+                return false;
+            }
+            return true;
+        }
+    }
+}
Index: /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmServerPreference.java
===================================================================
--- /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmServerPreference.java	(revision 33621)
+++ /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/gui/WikosmServerPreference.java	(revision 33621)
@@ -0,0 +1,70 @@
+// License: GPL. For details, see LICENSE file.
+package org.wikipedia.gui;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+import java.awt.GridBagLayout;
+
+import javax.swing.BorderFactory;
+import javax.swing.Box;
+import javax.swing.JLabel;
+import javax.swing.JPanel;
+
+import org.openstreetmap.josm.gui.preferences.PreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.PreferenceSettingFactory;
+import org.openstreetmap.josm.gui.preferences.PreferenceTabbedPane;
+import org.openstreetmap.josm.gui.preferences.SubPreferenceSetting;
+import org.openstreetmap.josm.gui.preferences.TabPreferenceSetting;
+import org.openstreetmap.josm.gui.widgets.HistoryComboBox;
+import org.openstreetmap.josm.tools.GBC;
+import org.wikipedia.io.WikosmDownloadReader;
+
+/**
+ * Preferences related to Wikosm API servers.
+ */
+public class WikosmServerPreference implements SubPreferenceSetting {
+
+    private final HistoryComboBox wikosmServer = new HistoryComboBox();
+
+    /**
+     * Factory used to create a new {@link WikosmServerPreference}.
+     */
+    public static class Factory implements PreferenceSettingFactory {
+        @Override
+        public PreferenceSetting createPreferenceSetting() {
+            return new WikosmServerPreference();
+        }
+    }
+
+    @Override
+    public TabPreferenceSetting getTabPreferenceSetting(PreferenceTabbedPane gui) {
+        return gui.getServerPreference();
+    }
+
+    @Override
+    public void addGui(PreferenceTabbedPane gui) {
+        final JPanel panel = new JPanel(new GridBagLayout());
+
+        panel.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
+        panel.add(new JLabel(tr("Server: ")), GBC.std().insets(5, 5, 5, 5));
+        panel.add(wikosmServer, GBC.eop().fill(GBC.HORIZONTAL));
+        wikosmServer.setPossibleItems(WikosmDownloadReader.WIKOSM_SERVER_HISTORY.get());
+        wikosmServer.setText(WikosmDownloadReader.WIKOSM_SERVER.get());
+
+        panel.add(Box.createVerticalGlue(), GBC.eol().fill());
+
+        getTabPreferenceSetting(gui).addSubTab(this, tr("Wikidata+OSM server"), panel);
+    }
+
+    @Override
+    public boolean ok() {
+        WikosmDownloadReader.WIKOSM_SERVER.put(wikosmServer.getText());
+        WikosmDownloadReader.WIKOSM_SERVER_HISTORY.put(wikosmServer.getHistory());
+        return false;
+    }
+
+    @Override
+    public boolean isExpert() {
+        return true;
+    }
+}
Index: /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/io/WikosmDownloadReader.java
===================================================================
--- /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/io/WikosmDownloadReader.java	(revision 33621)
+++ /applications/editors/josm/plugins/wikipedia/src/org/wikipedia/io/WikosmDownloadReader.java	(revision 33621)
@@ -0,0 +1,283 @@
+// License: GPL. For details, see LICENSE file.
+package org.wikipedia.io;
+
+import org.openstreetmap.josm.actions.DownloadPrimitiveAction;
+import org.openstreetmap.josm.data.Bounds;
+import org.openstreetmap.josm.data.DataSource;
+import org.openstreetmap.josm.data.coor.LatLon;
+import org.openstreetmap.josm.data.osm.*;
+import org.openstreetmap.josm.data.preferences.ListProperty;
+import org.openstreetmap.josm.data.preferences.StringProperty;
+import org.openstreetmap.josm.gui.progress.ProgressMonitor;
+import org.openstreetmap.josm.io.*;
+import org.openstreetmap.josm.io.NameFinder.SearchResult;
+import org.openstreetmap.josm.tools.HttpClient;
+import org.openstreetmap.josm.tools.Logging;
+import org.openstreetmap.josm.tools.Utils;
+
+import javax.json.Json;
+import javax.json.JsonArray;
+import javax.json.JsonObject;
+import javax.json.JsonValue;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.time.Duration;
+import java.time.LocalDateTime;
+import java.time.Period;
+import java.time.ZoneOffset;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import static org.openstreetmap.josm.tools.I18n.tr;
+
+/**
+ * Read content from an Wikosm server.
+ */
+public class WikosmDownloadReader extends BoundingBoxDownloader {
+
+    /**
+     * Property for current Wikosm server.
+     */
+    public static final StringProperty WIKOSM_SERVER = new StringProperty("download.wikosm.server",
+            "http://88.99.164.208/bigdata/namespace/wdq/sparql");
+    /**
+     * Property for list of known Wikosm servers.
+     */
+    public static final ListProperty WIKOSM_SERVER_HISTORY = new ListProperty("download.wikosm.servers",
+            Arrays.asList("http://88.99.164.208/bigdata/namespace/wdq/sparql"));
+
+    private static final String DATA_PREFIX = "?query=";
+
+    private final String wikosmServer;
+    private final String wikosmQuery;
+    private final boolean asNewLayer;
+    private final boolean downloadReferrers;
+    private final boolean downloadFull;
+
+    /**
+     * Constructs a new {@code WikosmDownloadReader}.
+     *
+     * @param downloadArea The area to download
+     * @param wikosmServer The Wikosm server to use
+     * @param wikosmQuery  The Wikosm query
+     */
+    public WikosmDownloadReader(Bounds downloadArea, String wikosmServer, String wikosmQuery,
+                                boolean asNewLayer, boolean downloadReferrers, boolean downloadFull) {
+        super(downloadArea);
+        setDoAuthenticate(false);
+        this.wikosmServer = wikosmServer;
+        this.wikosmQuery = wikosmQuery.trim();
+        this.asNewLayer = asNewLayer;
+        this.downloadReferrers = downloadReferrers;
+        this.downloadFull = downloadFull;
+    }
+
+    @Override
+    protected String getBaseUrl() {
+        return wikosmServer;
+    }
+
+    @Override
+    protected String getRequestForBbox(double lon1, double lat1, double lon2, double lat2) {
+        final String query = this.wikosmQuery
+                .replace("{{boxParams}}", bbox(lon1, lat1, lon2, lat2))
+                .replace("{{center}}", center(lon1, lat1, lon2, lat2));
+        return DATA_PREFIX + Utils.encodeUrl(query);
+    }
+
+    private static String bbox(double lon1, double lat1, double lon2, double lat2) {
+        return "\nbd:serviceParam wikibase:cornerWest " + point(lon1, lat1) + "." +
+                "bd:serviceParam wikibase:cornerEast " + point(lon2, lat2) + ".\n";
+    }
+
+    private static String center(double lon1, double lat1, double lon2, double lat2) {
+        LatLon c = new BBox(lon1, lat1, lon2, lat2).getCenter();
+        return point(c.lon(), c.lat());
+    }
+
+    private static String point(double lon, double lat) {
+        return "Point(\"" + lon + " " + lat + "\")^^geo:wktLiteral";
+    }
+
+    static String date(String humanDuration, LocalDateTime from) {
+        // Convert to ISO 8601. Replace months by X temporarily to avoid conflict with minutes
+        String duration = humanDuration.toLowerCase(Locale.ENGLISH).replace(" ", "")
+                .replaceAll("years?", "Y").replaceAll("months?", "X").replaceAll("weeks?", "W")
+                .replaceAll("days?", "D").replaceAll("hours?", "H").replaceAll("minutes?", "M").replaceAll("seconds?", "S");
+        Matcher matcher = Pattern.compile(
+                "((?:[0-9]+Y)?(?:[0-9]+X)?(?:[0-9]+W)?)"+
+                "((?:[0-9]+D)?)" +
+                "((?:[0-9]+H)?(?:[0-9]+M)?(?:[0-9]+(?:[.,][0-9]{0,9})?S)?)?").matcher(duration);
+        boolean javaPer = false;
+        boolean javaDur = false;
+        if (matcher.matches()) {
+            javaPer = matcher.group(1) != null && !matcher.group(1).isEmpty();
+            javaDur = matcher.group(3) != null && !matcher.group(3).isEmpty();
+            duration = 'P' + matcher.group(1).replace('X', 'M') + matcher.group(2);
+            if (javaDur) {
+                duration += 'T' + matcher.group(3);
+            }
+        }
+
+        // Duration is now a full ISO 8601 duration string. Unfortunately Java does not allow to parse it entirely.
+        // We must split the "period" (years, months, weeks, days) from the "duration" (days, hours, minutes, seconds).
+        Period p = null;
+        Duration d = null;
+        int idx = duration.indexOf('T');
+        if (javaPer) {
+            p = Period.parse(javaDur ? duration.substring(0, idx) : duration);
+        }
+        if (javaDur) {
+            d = Duration.parse(javaPer ? 'P' + duration.substring(idx, duration.length()) : duration);
+        } else if (!javaPer) {
+            d = Duration.parse(duration);
+        }
+
+        // Now that period and duration are known, compute the correct date/time
+        LocalDateTime dt = from;
+        if (p != null) {
+            dt = dt.minus(p);
+        }
+        if (d != null) {
+            dt = dt.minus(d);
+        }
+
+        // Returns the date/time formatted in ISO 8601
+        return dt.toInstant(ZoneOffset.UTC).toString();
+    }
+
+    private static SearchResult searchName(String area) throws IOException {
+        return NameFinder.queryNominatim(area).stream().filter(
+                x -> !OsmPrimitiveType.NODE.equals(x.getOsmId().getType())).iterator().next();
+    }
+
+    static String geocodeArea(String area) throws IOException {
+        // Offsets defined in https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#By_element_id
+        final EnumMap<OsmPrimitiveType, Long> idOffset = new EnumMap<>(OsmPrimitiveType.class);
+        idOffset.put(OsmPrimitiveType.NODE, 0L);
+        idOffset.put(OsmPrimitiveType.WAY, 2_400_000_000L);
+        idOffset.put(OsmPrimitiveType.RELATION, 3_600_000_000L);
+        final PrimitiveId osmId = searchName(area).getOsmId();
+        return String.format("area(%d)", osmId.getUniqueId() + idOffset.get(osmId.getType()));
+    }
+
+    static String geocodeBbox(String area) throws IOException {
+        Bounds bounds = searchName(area).getBounds();
+        return bounds.getMinLat() + "," + bounds.getMinLon() + "," + bounds.getMaxLat() + "," + bounds.getMaxLon();
+    }
+
+    static String geocodeCoords(String area) throws IOException {
+        SearchResult result = searchName(area);
+        return result.getLat() + "," + result.getLon();
+    }
+
+    static String geocodeId(String area) throws IOException {
+        PrimitiveId osmId = searchName(area).getOsmId();
+        return String.format("%s(%d)", osmId.getType().getAPIName(), osmId.getUniqueId());
+    }
+
+    @Override
+    protected InputStream getInputStreamRaw(String urlStr, ProgressMonitor progressMonitor, String reason,
+                                            boolean uncompressAccordingToContentDisposition) throws OsmTransferException {
+        try {
+            return super.getInputStreamRaw(urlStr, progressMonitor, reason, uncompressAccordingToContentDisposition);
+        } catch (OsmApiException ex) {
+//            final String errorIndicator = "Error</strong>: ";
+//            if (ex.getMessage() != null && ex.getMessage().contains(errorIndicator)) {
+//                final String errorPlusRest = ex.getMessage().split(errorIndicator)[1];
+//                if (errorPlusRest != null) {
+//                    ex.setErrorHeader(errorPlusRest.split("</")[0].replaceAll(".*::request_read_and_idx::", ""));
+//                }
+//            }
+            throw ex;
+        }
+    }
+
+    @Override
+    protected void adaptRequest(HttpClient request) {
+        // see https://wiki.openstreetmap.org/wiki/Overpass_API/Overpass_QL#timeout
+        final Matcher timeoutMatcher = Pattern.compile("#timeout:(\\d+)").matcher(wikosmQuery);
+        final int timeout;
+        if (timeoutMatcher.find()) {
+            timeout = (int) TimeUnit.SECONDS.toMillis(Integer.parseInt(timeoutMatcher.group(1)));
+        } else {
+            timeout = (int) TimeUnit.MINUTES.toMillis(3);
+        }
+        request.setConnectTimeout(timeout);
+        request.setReadTimeout(timeout);
+        request.setAccept("application/sparql-results+json");
+    }
+
+    @Override
+    protected String getTaskName() {
+        return tr("Contacting Server...");
+    }
+
+    @Override
+    protected DataSet parseDataSet(InputStream source, ProgressMonitor progressMonitor) throws IllegalDataException {
+        try {
+            List<PrimitiveId> ids = getPrimitiveIds(source);
+
+            // REVIEW: this seems like a bad way to initiate download from parsing, and makes it hard to test
+            DownloadPrimitiveAction.processItems(asNewLayer, ids, downloadReferrers, downloadFull);
+        } catch (IOException e) {
+            Logging.error(e);
+        }
+
+        return null;
+    }
+
+    static List<PrimitiveId> getPrimitiveIds(InputStream source) throws UnsupportedEncodingException {
+        Pattern uriPattern = Pattern.compile("^https://www\\.openstreetmap\\.org/(node|way|relation)/(\\d+)");
+        List<PrimitiveId> ids = new ArrayList<>();
+
+        JsonArray results = Json.createReader(new InputStreamReader(source, "UTF-8"))
+                .readObject()
+                .getJsonObject("results")
+                .getJsonArray("bindings");
+
+        for (JsonObject row : results.getValuesAs(JsonObject.class)) {
+            for (JsonValue column : row.values()) {
+                JsonObject columnObj = (JsonObject) column;
+                if (columnObj.getString("type").equals("uri"))  {
+                    Matcher matcher = uriPattern.matcher(columnObj.getString("value"));
+                    if (matcher.matches()) {
+                        ids.add(new SimplePrimitiveId(Long.parseLong(matcher.group(2)),
+                                OsmPrimitiveType.from(matcher.group(1))));
+                    }
+                }
+            }
+        }
+
+        return ids;
+    }
+
+    @Override
+    public DataSet parseOsm(ProgressMonitor progressMonitor) throws OsmTransferException {
+
+        DataSet ds = super.parseOsm(progressMonitor);
+
+        // add bounds if necessary (note that Wikosm API does not return bounds in the response XML)
+        if (ds != null && ds.getDataSources().isEmpty() && wikosmQuery.contains("{{bbox}}")) {
+            if (crosses180th) {
+                Bounds bounds = new Bounds(lat1, lon1, lat2, 180.0);
+                DataSource src = new DataSource(bounds, getBaseUrl());
+                ds.addDataSource(src);
+
+                bounds = new Bounds(lat1, -180.0, lat2, lon2);
+                src = new DataSource(bounds, getBaseUrl());
+                ds.addDataSource(src);
+            } else {
+                Bounds bounds = new Bounds(lat1, lon1, lat2, lon2);
+                DataSource src = new DataSource(bounds, getBaseUrl());
+                ds.addDataSource(src);
+            }
+        }
+
+        return ds;
+    }
+}
