Blob Blame History Raw
/*
 *  Copyright 2011-2016 Bastien Nocera
 *  Copyright 2016 Collabora Ltd.
 *
 * The geocode-glib library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Library General Public License as
 * published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 *
 * The geocode-glib library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	 See the GNU
 * Library General Public License for more details.
 *
 * You should have received a copy of the GNU Library General Public
 * License along with the Gnome Library; see the file COPYING.LIB.  If not,
 * write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
 * Boston, MA 02110-1301  USA.
 *
 * Authors: Bastien Nocera <hadess@hadess.net>
 *          Aleksander Morgado <aleksander.morgado@collabora.co.uk>
 *          Philip Withnall <philip.withnall@collabora.co.uk>
 */

#include <gio/gio.h>
#include <json-glib/json-glib.h>
#include <libsoup/soup.h>
#include <stdlib.h>
#include <string.h>

#include "geocode-glib-private.h"
#include "geocode-glib.h"
#include "geocode-nominatim.h"

/**
 * SECTION:geocode-nominatim
 * @short_description: Geocoding resolver using a Nominatim web service
 * @include: geocode-glib/geocode-glib.h
 *
 * Contains functions for geocoding using the
 * [OSM Nominatim APIs](http://wiki.openstreetmap.org/wiki/Nominatim) exposed
 * by a Nominatim server at a given URI. By default, the GNOME Nominatim server
 * is used, but other server details may be given when constructing a
 * #GeocodeNominatim.
 *
 * Since: 3.23.1
 */

typedef enum {
	PROP_BASE_URL = 1,
	PROP_MAINTAINER_EMAIL_ADDRESS,
	PROP_USER_AGENT,
} GeocodeNominatimProperty;

static GParamSpec *properties[PROP_USER_AGENT + 1];

typedef struct {
	char *base_url;
	char *maintainer_email_address;
	char *user_agent;
} GeocodeNominatimPrivate;

static void geocode_backend_iface_init (GeocodeBackendInterface *iface);

G_DEFINE_TYPE_WITH_CODE (GeocodeNominatim, geocode_nominatim, G_TYPE_OBJECT,
                         G_ADD_PRIVATE (GeocodeNominatim)
                         G_IMPLEMENT_INTERFACE (GEOCODE_TYPE_BACKEND,
                                                geocode_backend_iface_init))

/******************************************************************************/

static void _geocode_read_nominatim_attributes (JsonReader *reader,
                                                GHashTable *ht);

static struct {
	const char *tp_attr;
	const char *gc_attr; /* NULL to ignore */
} attrs_map[] = {
	/* See http://xmpp.org/extensions/xep-0080.html: */
	{ "countrycode", NULL },
	{ "country", "country" },
	{ "region", "state" },
	{ "county", "county" },
	{ "locality", "city" },
	{ "area", NULL },
	{ "postalcode", "postalcode" },
	{ "street", "street" },
	{ "building", NULL },
	{ "floor", NULL },
	{ "room",  NULL },
	{ "text", NULL },
	{ "description", NULL },
	{ "uri", NULL },
	{ "language", "accept-language" },

	/* Custom keys which are passed through: */
	{ "location", "location" },
	{ "limit", "limit" },
};

static const char *
tp_attr_to_gc_attr (const char *attr,
		    gboolean   *found)
{
	guint i;

	*found = FALSE;

	for (i = 0; i < G_N_ELEMENTS (attrs_map); i++) {
		if (g_str_equal (attr, attrs_map[i].tp_attr)){
			*found = TRUE;
			return attrs_map[i].gc_attr;
		}
	}

	return NULL;
}

static GHashTable *
geocode_forward_fill_params (GHashTable *params)
{
	GHashTable *params_out = NULL;
	GHashTableIter iter;
	GValue *value;
	const char *key;

	params_out = g_hash_table_new_full (g_str_hash, g_str_equal,
	                                    g_free, g_free);

	g_hash_table_iter_init (&iter, params);
	while (g_hash_table_iter_next (&iter, (gpointer *) &key, (gpointer *) &value)) {
		gboolean found;
		const char *gc_attr;
		char *str = NULL;
		GValue string_value = G_VALUE_INIT;

		gc_attr = tp_attr_to_gc_attr (key, &found);
		if (found == FALSE) {
			g_warning ("XEP attribute '%s' unhandled", key);
			continue;
		}
		if (gc_attr == NULL)
			continue;

		g_value_init (&string_value, G_TYPE_STRING);
		g_assert (g_value_transform (value, &string_value));
		str = g_value_dup_string (&string_value);
		g_value_unset (&string_value);

		if (str == NULL)
			continue;

		g_return_val_if_fail (g_utf8_validate (str, -1, NULL), NULL);

		g_hash_table_insert (params_out,
		                     g_strdup (gc_attr),
		                     str);
	}

	return params_out;
}

static gchar *
get_search_uri_for_params (GeocodeNominatim  *self,
                           GHashTable        *params,
                           GError           **error)
{
	GeocodeNominatimPrivate *priv;
	GHashTable *ht;
	char *lang;
	char *encoded_params;
	char *uri;
        guint8 i;
        gboolean query_possible = FALSE;
        char *location;
        const char *allowed_attributes[] = { "country",
                                             "region",
                                             "county",
                                             "locality",
                                             "postalcode",
                                             "street",
                                             "location",
                                             NULL };

	priv = geocode_nominatim_get_instance_private (self);

        /* Make sure we have at least one parameter that Nominatim allows querying for */
	for (i = 0; allowed_attributes[i] != NULL; i++) {
	        if (g_hash_table_lookup (params, allowed_attributes[i]) != NULL) {
			query_possible = TRUE;
			break;
		}
	}

        if (!query_possible) {
                char *str;

                str = g_strjoinv (", ", (char **) allowed_attributes);
                g_set_error (error, GEOCODE_ERROR, GEOCODE_ERROR_INVALID_ARGUMENTS,
                             "Only following parameters supported: %s", str);
                g_free (str);

		return NULL;
	}

	/* Prepare the query parameters */
	ht = _geocode_glib_dup_hash_table (params);
	g_hash_table_insert (ht, (gpointer) "format", (gpointer) "jsonv2");
	g_hash_table_insert (ht, (gpointer) "email", (gpointer) priv->maintainer_email_address);
	g_hash_table_insert (ht, (gpointer) "addressdetails", (gpointer) "1");

	lang = NULL;
	if (g_hash_table_lookup (ht, "accept-language") == NULL) {
		lang = _geocode_object_get_lang ();
		if (lang)
			g_hash_table_insert (ht, (gpointer) "accept-language", lang);
	}

        location = g_strdup (g_hash_table_lookup (ht, "location"));
        g_hash_table_remove (ht, "location");

	if (location == NULL)
		g_hash_table_insert (ht, (gpointer) "limit", (gpointer) "1");
	else if (!g_hash_table_contains (ht, "limit"))
		g_hash_table_insert (ht, (gpointer) "limit",
		                     (gpointer) G_STRINGIFY (DEFAULT_ANSWER_COUNT));

	if (location == NULL)
		g_hash_table_remove (ht, "bounded");
	else if (!g_hash_table_contains (ht, "bounded"))
		g_hash_table_insert (ht, (gpointer) "bounded", (gpointer) "0");

	if (location != NULL)
		g_hash_table_insert (ht, (gpointer) "q", location);

	encoded_params = soup_form_encode_hash (ht);
	g_hash_table_unref (ht);
	g_free (lang);
	g_free (location);

	uri = g_strdup_printf ("%s/search?%s", priv->base_url, encoded_params);
	g_free (encoded_params);

	return uri;
}

static struct {
	const char *nominatim_attr;
        const char *place_prop; /* NULL to ignore */
} nominatim_to_place_map[] = {
        { "license", NULL },
        { "osm_id", "osm-id" },
        { "lat", NULL },
        { "lon", NULL },
        { "display_name", NULL },
        { "house_number", "building" },
        { "road", "street" },
        { "suburb", "area" },
        { "city",  "town" },
        { "village",  "town" },
        { "county", "county" },
        { "state_district", "administrative-area" },
        { "state", "state" },
        { "postcode", "postal-code" },
        { "country", "country" },
        { "country_code", "country-code" },
        { "continent", "continent" },
        { "address", NULL },
};

static void
fill_place_from_entry (const char   *key,
                       const char   *value,
                       GeocodePlace *place)
{
        guint i;

        for (i = 0; i < G_N_ELEMENTS (nominatim_to_place_map); i++) {
                if (g_str_equal (key, nominatim_to_place_map[i].nominatim_attr)){
                        g_object_set (G_OBJECT (place),
                                      nominatim_to_place_map[i].place_prop,
                                      value,
                                      NULL);
                        break;
                }
        }

        if (g_str_equal (key, "osm_type")) {
                gpointer ref = g_type_class_ref (geocode_place_osm_type_get_type ());
                GEnumClass *class = G_ENUM_CLASS (ref);
                GEnumValue *evalue = g_enum_get_value_by_nick (class, value);

                if (evalue)
                        g_object_set (G_OBJECT (place), "osm-type", evalue->value, NULL);
                else
                        g_warning ("Unsupported osm-type %s", value);

                g_type_class_unref (ref);
        }
}

static gboolean
node_free_func (GNode    *node,
		gpointer  user_data)
{
	/* Leaf nodes are GeocodeLocation objects
	 * which we reuse for the results */
	if (G_NODE_IS_LEAF (node) == FALSE)
		g_free (node->data);

	return FALSE;
}

static const char *place_attributes[] = {
	"country",
	"state",
	"county",
	"state_district",
	"postcode",
	"city",
	"suburb",
	"village",
};

static GeocodePlaceType
get_place_type_from_attributes (GHashTable *ht)
{
        char *category, *type;
        GeocodePlaceType place_type = GEOCODE_PLACE_TYPE_UNKNOWN;

        category = g_hash_table_lookup (ht, "category");
        type = g_hash_table_lookup (ht, "type");

        if (g_strcmp0 (category, "place") == 0) {
                if (g_strcmp0 (type, "house") == 0 ||
                    g_strcmp0 (type, "building") == 0 ||
                    g_strcmp0 (type, "residential") == 0 ||
                    g_strcmp0 (type, "plaza") == 0 ||
                    g_strcmp0 (type, "office") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_BUILDING;
                else if (g_strcmp0 (type, "estate") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_ESTATE;
                else if (g_strcmp0 (type, "town") == 0 ||
                         g_strcmp0 (type, "city") == 0 ||
                         g_strcmp0 (type, "hamlet") == 0 ||
                         g_strcmp0 (type, "isolated_dwelling") == 0 ||
                         g_strcmp0 (type, "village") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_TOWN;
                else if (g_strcmp0 (type, "suburb") == 0 ||
                         g_strcmp0 (type, "neighbourhood") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_SUBURB;
                else if (g_strcmp0 (type, "state") == 0 ||
                         g_strcmp0 (type, "region") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_STATE;
                else if (g_strcmp0 (type, "farm") == 0 ||
                         g_strcmp0 (type, "forest") == 0 ||
                         g_strcmp0 (type, "valey") == 0 ||
                         g_strcmp0 (type, "park") == 0 ||
                         g_strcmp0 (type, "hill") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_LAND_FEATURE;
                else if (g_strcmp0 (type, "island") == 0 ||
                         g_strcmp0 (type, "islet") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_ISLAND;
                else if (g_strcmp0 (type, "country") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_COUNTRY;
                else if (g_strcmp0 (type, "continent") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_CONTINENT;
                else if (g_strcmp0 (type, "lake") == 0 ||
                         g_strcmp0 (type, "bay") == 0 ||
                         g_strcmp0 (type, "river") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_DRAINAGE;
                else if (g_strcmp0 (type, "sea") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_SEA;
                else if (g_strcmp0 (type, "ocean") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_OCEAN;
        } else if (g_strcmp0 (category, "highway") == 0) {
                if (g_strcmp0 (type, "motorway") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_MOTORWAY;
                else if (g_strcmp0 (type, "bus_stop") == 0)
                        place_type =  GEOCODE_PLACE_TYPE_BUS_STOP;
                else
                        place_type =  GEOCODE_PLACE_TYPE_STREET;
        } else if (g_strcmp0 (category, "railway") == 0) {
                if (g_strcmp0 (type, "station") == 0 ||
                    g_strcmp0 (type, "halt") == 0)
                        place_type = GEOCODE_PLACE_TYPE_RAILWAY_STATION;
                else if (g_strcmp0 (type, "tram_stop") == 0)
                        place_type = GEOCODE_PLACE_TYPE_LIGHT_RAIL_STATION;
        } else if (g_strcmp0 (category, "waterway") == 0) {
                place_type =  GEOCODE_PLACE_TYPE_DRAINAGE;
        } else if (g_strcmp0 (category, "boundary") == 0) {
                if (g_strcmp0 (type, "administrative") == 0) {
                        int rank;

                        rank = atoi (g_hash_table_lookup (ht, "place_rank"));
                        if (rank < 2)
                                place_type =  GEOCODE_PLACE_TYPE_UNKNOWN;

                        if (rank == 28)
                                place_type =  GEOCODE_PLACE_TYPE_BUILDING;
                        else if (rank == 16)
                                place_type =  GEOCODE_PLACE_TYPE_TOWN;
                        else if (rank == 12)
                                place_type =  GEOCODE_PLACE_TYPE_COUNTY;
                        else if (rank == 10 || rank == 8)
                                place_type =  GEOCODE_PLACE_TYPE_STATE;
                        else if (rank == 4)
                                place_type =  GEOCODE_PLACE_TYPE_COUNTRY;
                }
        } else if (g_strcmp0 (category, "amenity") == 0) {
                if (g_strcmp0 (type, "school") == 0)
                        place_type = GEOCODE_PLACE_TYPE_SCHOOL;
                else if (g_strcmp0 (type, "place_of_worship") == 0)
                        place_type = GEOCODE_PLACE_TYPE_PLACE_OF_WORSHIP;
                else if (g_strcmp0 (type, "restaurant") == 0)
                        place_type = GEOCODE_PLACE_TYPE_RESTAURANT;
                else if (g_strcmp0 (type, "bar") == 0 ||
                         g_strcmp0 (type, "pub") == 0)
                        place_type = GEOCODE_PLACE_TYPE_BAR;
        } else if (g_strcmp0 (category, "aeroway") == 0) {
                if (g_strcmp0 (type, "aerodrome") == 0)
                        place_type = GEOCODE_PLACE_TYPE_AIRPORT;
        }

        return place_type;
}

static GeocodePlace *
_geocode_create_place_from_attributes (GHashTable *ht)
{
        GeocodePlace *place;
        GeocodeLocation *loc = NULL;
        const char *name, *street, *building, *bbox_corner;
        GeocodePlaceType place_type;
        gdouble longitude, latitude;

        place_type = get_place_type_from_attributes (ht);

        name = g_hash_table_lookup (ht, "name");
        if (name == NULL)
                name = g_hash_table_lookup (ht, "display_name");

        place = geocode_place_new (name, place_type);

        /* If one corner exists, then all exists */
        bbox_corner = g_hash_table_lookup (ht, "boundingbox-top");
        if (bbox_corner != NULL) {
            GeocodeBoundingBox *bbox;
            gdouble top, bottom, left, right;

            top = g_ascii_strtod (bbox_corner, NULL);

            bbox_corner = g_hash_table_lookup (ht, "boundingbox-bottom");
            bottom = g_ascii_strtod (bbox_corner, NULL);

            bbox_corner = g_hash_table_lookup (ht, "boundingbox-left");
            left = g_ascii_strtod (bbox_corner, NULL);

            bbox_corner = g_hash_table_lookup (ht, "boundingbox-right");
            right = g_ascii_strtod (bbox_corner, NULL);

            bbox = geocode_bounding_box_new (top, bottom, left, right);
            geocode_place_set_bounding_box (place, bbox);
            g_object_unref (bbox);
        }

        /* Nominatim doesn't give us street addresses as such */
        street = g_hash_table_lookup (ht, "road");
        building = g_hash_table_lookup (ht, "house_number");
        if (street != NULL && building != NULL) {
            char *address;
            gboolean number_after;

            number_after = _geocode_object_is_number_after_street ();
            address = g_strdup_printf ("%s %s",
                                       number_after ? street : building,
                                       number_after ? building : street);
            geocode_place_set_street_address (place, address);
            g_free (address);
        }

        g_hash_table_foreach (ht, (GHFunc) fill_place_from_entry, place);

        /* Get latitude and longitude and create GeocodeLocation object. */
        longitude = g_ascii_strtod (g_hash_table_lookup (ht, "lon"), NULL);
        latitude = g_ascii_strtod (g_hash_table_lookup (ht, "lat"), NULL);
        name = geocode_place_get_name (place);

        loc = geocode_location_new_with_description (latitude,
                                                     longitude,
                                                     GEOCODE_LOCATION_ACCURACY_UNKNOWN,
                                                     name);
        geocode_place_set_location (place, loc);
        g_object_unref (loc);

        return place;
}

static void
insert_place_into_tree (GNode *place_tree, GHashTable *ht)
{
	GNode *start = place_tree;
        GeocodePlace *place = NULL;
	char *attr_val = NULL;
	guint i;

	for (i = 0; i < G_N_ELEMENTS (place_attributes); i++) {
		GNode *child = NULL;

		attr_val = g_hash_table_lookup (ht, place_attributes[i]);
		if (!attr_val) {
			/* Add a dummy node if the attribute value is not
			 * available for the place */
			child = g_node_insert_data (start, -1, NULL);
		} else {
			/* If the attr value (eg for country United States)
			 * already exists, then keep on adding other attributes under that node. */
			child = g_node_first_child (start);
			while (child &&
			       child->data &&
			       g_ascii_strcasecmp (child->data, attr_val) != 0) {
				child = g_node_next_sibling (child);
			}
			if (!child) {
				/* create a new node */
				child = g_node_insert_data (start, -1, g_strdup (attr_val));
			}
		}
		start = child;
	}

        place = _geocode_create_place_from_attributes (ht);

        /* The leaf node of the tree is the GeocodePlace object, containing
         * associated GeocodePlace object */
	g_node_insert_data (start, -1, place);
}

static void
make_place_list_from_tree (GNode  *node,
                           char  **s_array,
                           GList **place_list,
                           int     i)
{
	GNode *child;

	if (node == NULL)
		return;

	if (G_NODE_IS_LEAF (node)) {
		GPtrArray *rev_s_array;
		GeocodePlace *place;
		GeocodeLocation *loc;
		char *name;
		int counter = 0;

		rev_s_array = g_ptr_array_new ();

		/* If leaf node, then add all the attributes in the s_array
		 * and set it to the description of the loc object */
		place = (GeocodePlace *) node->data;
		name = (char *) geocode_place_get_name (place);
		loc = geocode_place_get_location (place);

		/* To print the attributes in a meaningful manner
		 * reverse the s_array */
		g_ptr_array_add (rev_s_array, (gpointer) name);
		for (counter = 1; counter <= i; counter++)
			g_ptr_array_add (rev_s_array, s_array[i - counter]);
		g_ptr_array_add (rev_s_array, NULL);
		name = g_strjoinv (", ", (char **) rev_s_array->pdata);
		g_ptr_array_unref (rev_s_array);

		geocode_place_set_name (place, name);
		geocode_location_set_description (loc, name);
		g_free (name);

		*place_list = g_list_prepend (*place_list, place);
	} else {
                GNode *prev, *next;

                prev = g_node_prev_sibling (node);
                next = g_node_next_sibling (node);

		/* If there are other attributes with a different value,
		 * add those attributes to the string to differentiate them */
		if (node->data && ((prev && prev->data) || (next && next->data))) {
                        s_array[i] = node->data;
                        i++;
		}
	}

	for (child = node->children; child != NULL; child = child->next)
		make_place_list_from_tree (child, s_array, place_list, i);
}

GList *
_geocode_parse_search_json (const char *contents,
			     GError    **error)
{
	GList *ret;
	JsonParser *parser;
	JsonNode *root;
	JsonReader *reader;
	const GError *err = NULL;
	int num_places, i;
	GNode *place_tree;
	char *s_array[G_N_ELEMENTS (place_attributes)];

	g_debug ("%s: contents = %s", G_STRFUNC, contents);

	ret = NULL;

	parser = json_parser_new ();
	if (json_parser_load_from_data (parser, contents, -1, error) == FALSE) {
		g_object_unref (parser);
		return ret;
	}

	root = json_parser_get_root (parser);
	reader = json_reader_new (root);

	num_places = json_reader_count_elements (reader);
	if (num_places < 0)
		goto parse;
        if (num_places == 0) {
	        g_set_error_literal (error,
                                     GEOCODE_ERROR,
                                     GEOCODE_ERROR_NO_MATCHES,
                                     "No matches found for request");
		goto no_results;
        }

	place_tree = g_node_new (NULL);

	for (i = 0; i < num_places; i++) {
		GHashTable *ht;

		json_reader_read_element (reader, i);

                ht = g_hash_table_new_full (g_str_hash, g_str_equal,
				            g_free, g_free);
                _geocode_read_nominatim_attributes (reader, ht);

		/* Populate the tree with place details */
		insert_place_into_tree (place_tree, ht);

		g_hash_table_unref (ht);

		json_reader_end_element (reader);
	}

	make_place_list_from_tree (place_tree, s_array, &ret, 0);

	g_node_traverse (place_tree,
			 G_IN_ORDER,
			 G_TRAVERSE_ALL,
			 -1,
			 (GNodeTraverseFunc) node_free_func,
			 NULL);

	g_node_destroy (place_tree);

	g_object_unref (parser);
	g_object_unref (reader);
	ret = g_list_reverse (ret);

	return ret;
parse:
	err = json_reader_get_error (reader);
	g_set_error_literal (error, GEOCODE_ERROR, GEOCODE_ERROR_PARSE, err->message);
no_results:
	g_object_unref (parser);
	g_object_unref (reader);
	return NULL;
}

static GList *
geocode_nominatim_forward_search (GeocodeBackend  *backend,
                                  GHashTable      *params,
                                  GCancellable    *cancellable,
                                  GError         **error)
{
	GeocodeNominatim *self = GEOCODE_NOMINATIM (backend);
	char *contents;
	GHashTable *transformed_params = NULL;  /* (utf8, utf8) */
	GList *result = NULL;  /* (element-type GeocodePlace) */
	gchar *uri = NULL;

	transformed_params = geocode_forward_fill_params (params);
	uri = get_search_uri_for_params (self, transformed_params, error);
	g_hash_table_unref (transformed_params);

	if (uri == NULL)
		return NULL;

	contents = GEOCODE_NOMINATIM_GET_CLASS (self)->query (self,
	                                                      uri,
	                                                      cancellable,
	                                                      error);
	if (contents != NULL) {
		result = _geocode_parse_search_json (contents, error);
		g_free (contents);
	}

	g_free (uri);

	return result;
}

static void
on_forward_query_ready (GeocodeNominatim *self,
                        GAsyncResult     *res,
                        GTask            *task)
{
	GError *error = NULL;
	char *contents;
	GList *places;  /* (element-type GeocodePlace) */

	contents = GEOCODE_NOMINATIM_GET_CLASS (self)->query_finish (GEOCODE_NOMINATIM (self), res, &error);
	if (contents == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	places = _geocode_parse_search_json (contents, &error);
	g_free (contents);

	if (places == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	g_task_return_pointer (task, places, (GDestroyNotify) g_list_free);
	g_object_unref (task);
}

static void
geocode_nominatim_forward_search_async (GeocodeBackend      *backend,
                                        GHashTable          *params,
                                        GCancellable        *cancellable,
                                        GAsyncReadyCallback  callback,
                                        gpointer             user_data)
{
	GeocodeNominatim *self = GEOCODE_NOMINATIM (backend);
	GTask *task;
	GHashTable *transformed_params = NULL;  /* (utf8, utf8) */
	gchar *uri = NULL;
	GError *error = NULL;

	transformed_params = geocode_forward_fill_params (params);
	uri = get_search_uri_for_params (self, transformed_params, &error);
	g_hash_table_unref (transformed_params);

	if (error != NULL) {
		g_task_report_error (self, callback, user_data, NULL, error);
		return;
	}

	task = g_task_new (self, cancellable, callback, user_data);
	GEOCODE_NOMINATIM_GET_CLASS (self)->query_async (self,
	                                                 uri,
	                                                 cancellable,
	                                                 (GAsyncReadyCallback) on_forward_query_ready,
	                                                 g_object_ref (task));
	g_object_unref (task);
	g_free (uri);
}

static GList *
geocode_nominatim_forward_search_finish (GeocodeBackend  *backend,
                                         GAsyncResult    *res,
                                         GError         **error)
{
	return g_task_propagate_pointer (G_TASK (res), error);
}

/******************************************************************************/

static void
copy_item (char       *key,
           char       *value,
           GHashTable *ret)
{
	g_hash_table_insert (ret, key, value);
}

GHashTable *
_geocode_glib_dup_hash_table (GHashTable *ht)
{
	GHashTable *ret;

	ret = g_hash_table_new (g_str_hash, g_str_equal);
	g_hash_table_foreach (ht, (GHFunc) copy_item, ret);

	return ret;
}

static gchar *
get_resolve_uri_for_params (GeocodeNominatim  *self,
                            GHashTable        *orig_ht,
                            GError           **error)
{
	GHashTable *ht;
	char *locale;
	char *params, *uri;
	GeocodeNominatimPrivate *priv;
	const GValue *lat, *lon;
	char lat_str[G_ASCII_DTOSTR_BUF_SIZE];
	char lon_str[G_ASCII_DTOSTR_BUF_SIZE];

	priv = geocode_nominatim_get_instance_private (self);

	/* Make sure we have both lat and lon. */
	lat = g_hash_table_lookup (orig_ht, "lat");
	lon = g_hash_table_lookup (orig_ht, "lon");

	if (lat == NULL || lon == NULL) {
		g_set_error_literal (error, GEOCODE_ERROR, GEOCODE_ERROR_INVALID_ARGUMENTS,
		                     "Only following parameters supported: lat, lon");

		return NULL;
	}

	ht = g_hash_table_new_full (g_str_hash, g_str_equal, NULL, NULL);

	g_ascii_dtostr (lat_str, G_ASCII_DTOSTR_BUF_SIZE,
	                g_value_get_double (lat));
	g_ascii_dtostr (lon_str, G_ASCII_DTOSTR_BUF_SIZE,
	                g_value_get_double (lon));

	g_hash_table_insert (ht, (gpointer) "lat", lat_str);
	g_hash_table_insert (ht, (gpointer) "lon", lon_str);

	g_hash_table_insert (ht, (gpointer) "format", (gpointer) "json");
	g_hash_table_insert (ht, (gpointer) "email",
	                     (gpointer) priv->maintainer_email_address);
	g_hash_table_insert (ht, (gpointer) "addressdetails", (gpointer) "1");

	locale = NULL;
	if (g_hash_table_lookup (ht, "accept-language") == NULL) {
		locale = _geocode_object_get_lang ();
		if (locale)
			g_hash_table_insert (ht, (gpointer) "accept-language", locale);
	}

	{
		GHashTableIter iter;
		gpointer key, value;

		g_hash_table_iter_init (&iter, ht);
		while (g_hash_table_iter_next (&iter, &key, &value))
			g_debug ("%s: %s = %s", G_STRFUNC, (const gchar *) key, (const gchar *) value);
	}

	params = soup_form_encode_hash (ht);
	g_hash_table_unref (ht);
	g_free (locale);

	uri = g_strdup_printf ("%s/reverse?%s", priv->base_url, params);
	g_free (params);

	return uri;
}

static gchar *
geocode_nominatim_query_finish (GeocodeNominatim  *self,
                                GAsyncResult      *res,
                                GError           **error)
{
	return g_task_propagate_pointer (G_TASK (res), error);
}

static void
on_query_data_loaded (SoupSession *session,
                      SoupMessage *query,
                      GTask       *task)
{
	char *contents;

	if (query->status_code != SOUP_STATUS_OK)
		g_task_return_new_error (task,
		                         G_IO_ERROR,
		                         G_IO_ERROR_FAILED,
		                         "%s",
		                         query->reason_phrase ? query->reason_phrase : "Query failed");
	else {
		contents = g_strndup (query->response_body->data, query->response_body->length);
		_geocode_glib_cache_save (query, contents);
		g_task_return_pointer (task, contents, g_free);
	}

	g_object_unref (task);
}

static void
on_cache_data_loaded (GFile        *cache,
                      GAsyncResult *res,
                      GTask        *task)
{
	GeocodeNominatim *self;
	GeocodeNominatimPrivate *priv;
	char *contents;
	SoupSession *soup_session;

	self = g_task_get_source_object (task);
	priv = geocode_nominatim_get_instance_private (self);

	if (g_file_load_contents_finish (cache,
	                                 res,
	                                 &contents,
	                                 NULL,
	                                 NULL,
	                                 NULL)) {
		g_task_return_pointer (task, contents, g_free);
		g_object_unref (task);
		return;
	}

	soup_session = _geocode_glib_build_soup_session (priv->user_agent);
	soup_session_queue_message (soup_session,
	                            g_object_ref (g_task_get_task_data (task)),
	                            (SoupSessionCallback) on_query_data_loaded,
	                            task);
	g_object_unref (soup_session);
}

static void
geocode_nominatim_query_async (GeocodeNominatim    *self,
                               const gchar         *uri,
                               GCancellable        *cancellable,
                               GAsyncReadyCallback  callback,
                               gpointer             user_data)
{
	GTask *task;
	SoupSession *soup_session;
	SoupMessage *soup_query;
	char *cache_path;
	GeocodeNominatimPrivate *priv;

	priv = geocode_nominatim_get_instance_private (self);

	g_debug ("%s: uri = %s", G_STRFUNC, uri);

	task = g_task_new (self, cancellable, callback, user_data);

	soup_query = soup_message_new (SOUP_METHOD_GET, uri);
	g_task_set_task_data (task, soup_query, g_object_unref);

	cache_path = _geocode_glib_cache_path_for_query (soup_query);
	if (cache_path != NULL) {
		GFile *cache;

		cache = g_file_new_for_path (cache_path);
		g_file_load_contents_async (cache,
		                            cancellable,
		                            (GAsyncReadyCallback) on_cache_data_loaded,
		                            task);
		g_object_unref (cache);
		g_free (cache_path);
		return;
	}

	soup_session = _geocode_glib_build_soup_session (priv->user_agent);
	soup_session_queue_message (soup_session,
	                            g_object_ref (soup_query),
	                            (SoupSessionCallback) on_query_data_loaded,
	                            task);
	g_object_unref (soup_session);
}

static gchar *
geocode_nominatim_query (GeocodeNominatim  *self,
                         const gchar       *uri,
                         GCancellable      *cancellable,
                         GError           **error)
{
	SoupSession *soup_session;
	SoupMessage *soup_query;
	char *contents;
	GeocodeNominatimPrivate *priv;

	priv = geocode_nominatim_get_instance_private (self);

	g_debug ("%s: uri = %s", G_STRFUNC, uri);

	if (g_cancellable_set_error_if_cancelled (cancellable, error))
		return NULL;

	soup_session = _geocode_glib_build_soup_session (priv->user_agent);
	soup_query = soup_message_new (SOUP_METHOD_GET, uri);

	if (_geocode_glib_cache_load (soup_query, &contents) == FALSE) {
		if (soup_session_send_message (soup_session, soup_query) != SOUP_STATUS_OK) {
			g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED,
			                     soup_query->reason_phrase ? soup_query->reason_phrase : "Query failed");
			contents = NULL;
		} else {
			contents = g_strndup (soup_query->response_body->data, soup_query->response_body->length);
			_geocode_glib_cache_save (soup_query, contents);
		}
	}

	g_object_unref (soup_query);
	g_object_unref (soup_session);

	return contents;
}

/******************************************************************************/

static GList *
geocode_nominatim_reverse_resolve_finish (GeocodeBackend  *backend,
                                          GAsyncResult    *res,
                                          GError         **error)
{
	return g_task_propagate_pointer (G_TASK (res), error);
}

static void
insert_bounding_box_element (GHashTable *ht,
                             GType       value_type,
                             const char *name,
                             JsonReader *reader)
{
	if (value_type == G_TYPE_STRING) {
		const char *bbox_val;

		bbox_val = json_reader_get_string_value (reader);
		g_hash_table_insert (ht, g_strdup (name), g_strdup (bbox_val));
	} else if (value_type == G_TYPE_DOUBLE) {
		gdouble bbox_val;

		bbox_val = json_reader_get_double_value (reader);
		g_hash_table_insert(ht, g_strdup (name), g_strdup_printf ("%lf", bbox_val));
	} else if (value_type == G_TYPE_INT64) {
		gint64 bbox_val;

		bbox_val = json_reader_get_double_value (reader);
		g_hash_table_insert(ht, g_strdup (name), g_strdup_printf ("%"G_GINT64_FORMAT, bbox_val));
	} else {
		g_debug ("Unhandled node type %s for %s", g_type_name (value_type), name);
	}
}

static void
_geocode_read_nominatim_attributes (JsonReader *reader,
                                    GHashTable *ht)
{
	char **members;
	guint i;
	gboolean is_address;
	const char *house_number = NULL;

	is_address = (g_strcmp0 (json_reader_get_member_name (reader), "address") == 0);

	members = json_reader_list_members (reader);
	if (members == NULL) {
		json_reader_end_member (reader);
		return;
	}

	for (i = 0; members[i] != NULL; i++) {
		const char *value = NULL;

		json_reader_read_member (reader, members[i]);

		if (json_reader_is_value (reader)) {
			JsonNode *node = json_reader_get_value (reader);
			if (json_node_get_value_type (node) == G_TYPE_STRING) {
				value = json_node_get_string (node);
				if (value && *value == '\0')
					value = NULL;
			}
		}

		if (value != NULL) {
			g_hash_table_insert (ht, g_strdup (members[i]), g_strdup (value));

			if (i == 0 && is_address) {
				if (g_strcmp0 (members[i], "house_number") != 0)
					/* Since Nominatim doesn't give us a short name,
					 * we use the first component of address as name.
					 */
					g_hash_table_insert (ht, g_strdup ("name"), g_strdup (value));
				else
					house_number = value;
			} else if (house_number != NULL && g_strcmp0 (members[i], "road") == 0) {
				gboolean number_after;
				char *name;

				number_after = _geocode_object_is_number_after_street ();
				name = g_strdup_printf ("%s %s",
				                        number_after ? value : house_number,
				                        number_after ? house_number : value);
				g_hash_table_insert (ht, g_strdup ("name"), name);
			}
		} else if (g_strcmp0 (members[i], "boundingbox") == 0) {
			JsonNode *node;
			GType value_type;

			json_reader_read_element (reader, 0);
			node = json_reader_get_value (reader);
			value_type = json_node_get_value_type (node);

			insert_bounding_box_element (ht, value_type, "boundingbox-bottom", reader);
			json_reader_end_element (reader);

			json_reader_read_element (reader, 1);
			insert_bounding_box_element (ht, value_type, "boundingbox-top", reader);
			json_reader_end_element (reader);

			json_reader_read_element (reader, 2);
			insert_bounding_box_element (ht, value_type, "boundingbox-left", reader);
			json_reader_end_element (reader);

			json_reader_read_element (reader, 3);
			insert_bounding_box_element (ht, value_type, "boundingbox-right", reader);
			json_reader_end_element (reader);
		}
		json_reader_end_member (reader);
	}

	g_strfreev (members);

	if (json_reader_read_member (reader, "address"))
		_geocode_read_nominatim_attributes (reader, ht);
	json_reader_end_member (reader);
}

static GHashTable *
resolve_json (const char  *contents,
              GError     **error)
{
	GHashTable *ret = NULL;
	JsonParser *parser;
	JsonNode *root;
	JsonReader *reader;

	g_debug ("%s: contents = %s", G_STRFUNC, contents);

	parser = json_parser_new ();
	if (json_parser_load_from_data (parser, contents, -1, error) == FALSE) {
		g_object_unref (parser);
		return ret;
	}

	root = json_parser_get_root (parser);
	reader = json_reader_new (root);

	if (json_reader_read_member (reader, "error")) {
		const char *msg;

		msg = json_reader_get_string_value (reader);
		json_reader_end_member (reader);
		if (msg && *msg == '\0')
			msg = NULL;

		g_set_error_literal (error,
		                     GEOCODE_ERROR,
		                     GEOCODE_ERROR_NOT_SUPPORTED,
		                     msg ? msg : "Query not supported");
		g_object_unref (parser);
		g_object_unref (reader);
		return NULL;
	}

	ret = g_hash_table_new_full (g_str_hash, g_str_equal, g_free, g_free);
	_geocode_read_nominatim_attributes (reader, ret);

	g_object_unref (parser);
	g_object_unref (reader);

	return ret;
}

static void
places_list_free (GList *places)
{
	g_list_free_full (places, g_object_unref);
}

static void
on_reverse_query_ready (GeocodeNominatim *self,
                        GAsyncResult     *res,
                        GTask            *task)
{
	GError *error = NULL;
	char *contents;
	g_autoptr (GeocodePlace) place = NULL;
	GHashTable *attributes;

	contents = GEOCODE_NOMINATIM_GET_CLASS (self)->query_finish (GEOCODE_NOMINATIM (self), res, &error);
	if (contents == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	attributes = resolve_json (contents, &error);
	g_free (contents);

	if (attributes == NULL) {
		g_task_return_error (task, error);
		g_object_unref (task);
		return;
	}

	place = _geocode_create_place_from_attributes (attributes);
	g_hash_table_unref (attributes);

	g_task_return_pointer (task,
	                       g_list_prepend (NULL, g_object_ref (place)),
	                       (GDestroyNotify) places_list_free);
	g_object_unref (task);
}

static void
geocode_nominatim_reverse_resolve_async (GeocodeBackend      *self,
                                         GHashTable          *params,
                                         GCancellable        *cancellable,
                                         GAsyncReadyCallback  callback,
                                         gpointer             user_data)
{
	GTask *task;
	gchar *uri = NULL;
	GError *error = NULL;

	g_return_if_fail (GEOCODE_IS_BACKEND (self));
	g_return_if_fail (params != NULL);

	uri = get_resolve_uri_for_params (GEOCODE_NOMINATIM (self), params,
	                                  &error);

	if (error != NULL) {
		g_task_report_error (self, callback, user_data, NULL, error);
		return;
	}

	task = g_task_new (self, cancellable, callback, user_data);
	GEOCODE_NOMINATIM_GET_CLASS (self)->query_async (GEOCODE_NOMINATIM (self),
	                                                 uri,
	                                                 cancellable,
	                                                 (GAsyncReadyCallback) on_reverse_query_ready,
	                                                 g_object_ref (task));
	g_object_unref (task);
	g_free (uri);
}

static GList *
geocode_nominatim_reverse_resolve (GeocodeBackend  *self,
                                   GHashTable      *params,
                                   GCancellable    *cancellable,
                                   GError         **error)
{
	char *contents;
	GHashTable *result = NULL;
	g_autoptr (GeocodePlace) place = NULL;
	gchar *uri = NULL;

	g_return_val_if_fail (GEOCODE_IS_BACKEND (self), NULL);
	g_return_val_if_fail (params != NULL, NULL);

	uri = get_resolve_uri_for_params (GEOCODE_NOMINATIM (self), params,
	                                  error);

	if (uri == NULL)
		return NULL;

	contents = GEOCODE_NOMINATIM_GET_CLASS (self)->query (GEOCODE_NOMINATIM (self),
	                                                      uri,
	                                                      cancellable,
	                                                      error);
	if (contents != NULL) {
		result = resolve_json (contents, error);
		g_free (contents);
	}

	g_free (uri);

	if (result == NULL)
		return NULL;

	place = _geocode_create_place_from_attributes (result);
	g_hash_table_unref (result);

	return g_list_prepend (NULL, g_object_ref (place));
}

/******************************************************************************/

G_LOCK_DEFINE_STATIC (backend_nominatim_gnome_lock);
static GWeakRef backend_nominatim_gnome;

/**
 * geocode_nominatim_get_gnome:
 *
 * Gets a reference to the default Nominatim server on nominatim.gnome.org.
 *
 * This function is thread-safe.
 *
 * Returns: (transfer full): a new #GeocodeNominatim. Use g_object_unref() when done.
 *
 * Since: 3.23.1
 */
GeocodeNominatim *
geocode_nominatim_get_gnome (void)
{
	GeocodeNominatim *backend;

	G_LOCK (backend_nominatim_gnome_lock);
	backend = g_weak_ref_get (&backend_nominatim_gnome);
	if (backend == NULL) {
		backend = geocode_nominatim_new ("https://nominatim.gnome.org",
		                                 "zeeshanak@gnome.org");
		g_weak_ref_set (&backend_nominatim_gnome, backend);
	}
	G_UNLOCK (backend_nominatim_gnome_lock);

	return backend;
}

/******************************************************************************/

/**
 * geocode_nominatim_new:
 * @base_url: a the base URL of the Nominatim server.
 * @maintainer_email_address: the email address of the software maintainer.
 *
 * Creates a new backend implementation for an online Nominatim server. See
 * the documentation for #GeocodeNominatim:base-url and
 * #GeocodeNominatim:maintainer-email-address.
 *
 * Returns: (transfer full): a new #GeocodeNominatim. Use g_object_unref() when done.
 *
 * Since: 3.23.1
 */
GeocodeNominatim *
geocode_nominatim_new (const char *base_url,
                       const char *maintainer_email_address)
{
	g_return_val_if_fail (base_url != NULL, NULL);
	g_return_val_if_fail (maintainer_email_address != NULL, NULL);

	return GEOCODE_NOMINATIM (g_object_new (GEOCODE_TYPE_NOMINATIM,
	                                        "base-url", base_url,
	                                        "maintainer-email-address", maintainer_email_address,
	                                        NULL));
}

static void
geocode_nominatim_init (GeocodeNominatim *object)
{
}

static void
geocode_nominatim_constructed (GObject *object)
{
	GeocodeNominatimPrivate *priv;

	/* Chain up. */
	G_OBJECT_CLASS (geocode_nominatim_parent_class)->constructed (object);

	priv = geocode_nominatim_get_instance_private (GEOCODE_NOMINATIM (object));

	/* Ensure our mandatory construction properties have been passed. */
	g_assert (priv->base_url != NULL);
	g_assert (priv->maintainer_email_address != NULL);
}

static void
geocode_nominatim_get_property (GObject    *object,
                                guint       property_id,
                                GValue     *value,
                                GParamSpec *pspec)
{
	GeocodeNominatimPrivate *priv;

	priv = geocode_nominatim_get_instance_private (GEOCODE_NOMINATIM (object));

	switch ((GeocodeNominatimProperty) property_id) {
	case PROP_BASE_URL:
		g_value_set_string (value, priv->base_url);
		break;
	case PROP_MAINTAINER_EMAIL_ADDRESS:
		g_value_set_string (value, priv->maintainer_email_address);
		break;
	case PROP_USER_AGENT:
		g_value_set_string (value, priv->user_agent);
		break;
	default:
		/* We don't have any other property... */
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
		break;
	}
}

static void
geocode_nominatim_set_property (GObject      *object,
                                guint         property_id,
                                const GValue *value,
                                GParamSpec   *pspec)
{
	GeocodeNominatimPrivate *priv;

	priv = geocode_nominatim_get_instance_private (GEOCODE_NOMINATIM (object));

	switch ((GeocodeNominatimProperty) property_id) {
	case PROP_BASE_URL:
		/* Construct only. */
		g_assert (priv->base_url == NULL);
		priv->base_url = g_value_dup_string (value);
		break;
	case PROP_MAINTAINER_EMAIL_ADDRESS:
		/* Construct only. */
		g_assert (priv->maintainer_email_address == NULL);
		priv->maintainer_email_address = g_value_dup_string (value);
		break;
	case PROP_USER_AGENT:
		if (g_strcmp0 (priv->user_agent, g_value_get_string (value)) != 0) {
			g_free (priv->user_agent);
			priv->user_agent = g_value_dup_string (value);
			g_object_notify_by_pspec (object,
			                          properties[PROP_USER_AGENT]);
		}
		break;
	default:
		/* We don't have any other property... */
		G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
		break;
	}
}

static void
geocode_nominatim_finalize (GObject *object)
{
	GeocodeNominatimPrivate *priv;

	priv = geocode_nominatim_get_instance_private (GEOCODE_NOMINATIM (object));

	g_free (priv->base_url);
	g_free (priv->maintainer_email_address);
	g_free (priv->user_agent);

	G_OBJECT_CLASS (geocode_nominatim_parent_class)->finalize (object);
}

static void
geocode_backend_iface_init (GeocodeBackendInterface *iface)
{
	iface->forward_search         = geocode_nominatim_forward_search;
	iface->forward_search_async   = geocode_nominatim_forward_search_async;
	iface->forward_search_finish  = geocode_nominatim_forward_search_finish;

	iface->reverse_resolve        = geocode_nominatim_reverse_resolve;
	iface->reverse_resolve_async  = geocode_nominatim_reverse_resolve_async;
	iface->reverse_resolve_finish = geocode_nominatim_reverse_resolve_finish;
}

static void
geocode_nominatim_class_init (GeocodeNominatimClass *klass)
{
	GObjectClass *object_class = G_OBJECT_CLASS (klass);

	object_class->constructed  = geocode_nominatim_constructed;
	object_class->finalize     = geocode_nominatim_finalize;
	object_class->get_property = geocode_nominatim_get_property;
	object_class->set_property = geocode_nominatim_set_property;

	klass->query        = geocode_nominatim_query;
	klass->query_async  = geocode_nominatim_query_async;
	klass->query_finish = geocode_nominatim_query_finish;

	/**
	 * GeocodeNominatim:base-url:
	 *
	 * The base URL of the Nominatim service, for example
	 * `https://nominatim.example.org`.
	 *
	 * Since: 3.23.1
	 */
	properties[PROP_BASE_URL] = g_param_spec_string ("base-url",
	                                                 "Base URL",
	                                                 "Base URL of the Nominatim service",
	                                                 NULL,
	                                                 (G_PARAM_READWRITE |
	                                                  G_PARAM_CONSTRUCT_ONLY |
	                                                  G_PARAM_STATIC_STRINGS));

	/**
	 * GeocodeNominatim:maintainer-email-address:
	 *
	 * E-mail address of the maintainer of the software making the
	 * geocoding requests to the  Nominatim server. This is used to contact
	 * them in the event of a problem with their usage. See
	 * [the Nominatim API](http://wiki.openstreetmap.org/wiki/Nominatim).
	 *
	 * Since: 3.23.1
	 */
	properties[PROP_MAINTAINER_EMAIL_ADDRESS] =
	    g_param_spec_string ("maintainer-email-address",
	                         "Maintainer e-mail address",
	                         "E-mail address of the maintainer",
	                         NULL,
	                         (G_PARAM_READWRITE |
	                          G_PARAM_CONSTRUCT_ONLY |
	                          G_PARAM_STATIC_STRINGS));

	/**
	 * GeocodeNominatim:user-agent:
	 *
	 * User-Agent string to send with HTTP(S) requests, or %NULL to use the
	 * default user agent, which is derived from the geocode-glib version
	 * and #GApplication:id, for example: `geocode-glib/3.20 (MyAppId)`.
	 *
	 * As per the
	 * [Nominatim usage policy](http://wiki.openstreetmap.org/wiki/Nominatim_usage_policy),
	 * it should be set to a string which identifies the application which
	 * is using geocode-glib, and must be a valid
	 * [user agent](https://tools.ietf.org/html/rfc7231#section-5.5.3)
	 * string.
	 *
	 * Since: 3.23.1
	 */
	properties[PROP_USER_AGENT] = g_param_spec_string ("user-agent",
	                                                   "User agent",
	                                                   "User-Agent string to send with HTTP(S) requests",
	                                                   NULL,
	                                                   (G_PARAM_READWRITE |
	                                                    G_PARAM_STATIC_STRINGS));

	g_object_class_install_properties (object_class,
	                                   G_N_ELEMENTS (properties), properties);
}