diff --git a/frontend/src/components/EditPointInfo.vue b/frontend/src/components/EditPointInfo.vue
new file mode 100644
index 0000000..c5d78f2
--- /dev/null
+++ b/frontend/src/components/EditPointInfo.vue
@@ -0,0 +1,252 @@
+<template>
+  <form class="edit-point" @submit.prevent="$emit('edit', newValue)">
+    <div class="content">
+      <div class="row">
+        <label for="id">
+          <font-awesome-icon icon="fas fa-tag" />
+          <span class="description">ID</span>
+        </label>
+        <span class="value">
+          <input
+            name="id"
+            :value="point?.id"
+            type="text"
+            disabled />
+
+          <span class="buttons">
+            <button type="button"
+                    title="Copy ID"
+                    @click="copyToClipboard(point?.id?.toString() || '')">
+              <font-awesome-icon icon="fas fa-copy" />
+            </button>
+          </span>
+        </span>
+      </div>
+
+      <div class="row">
+        <label for="description">
+          <font-awesome-icon icon="fas fa-edit" />
+          <span class="description">Description</span>
+        </label>
+        <span class="value">
+          <!-- @vue-ignore -->
+          <textarea name="description" v-model="newValue.description" placeholder="Enter a description" />
+        </span>
+      </div>
+    </div>
+
+    <div class="row">
+      <label for="address">
+        <font-awesome-icon icon="fas fa-map-marked-alt" />
+        <span class="description">Address</span>
+      </label>
+      <span class="value">
+        <input
+            name="address"
+            v-model="newValue.address"
+            type="text"
+            placeholder="Enter an address" />
+      </span>
+    </div>
+
+    <div class="row">
+      <label for="locality">
+        <font-awesome-icon icon="fas fa-map-pin" />
+        <span class="description">Locality</span>
+      </label>
+      <span class="value">
+        <input
+            name="locality"
+            v-model="newValue.locality"
+            type="text"
+            placeholder="Enter a locality" />
+      </span>
+    </div>
+
+    <div class="row">
+      <label for="postalCode">
+        <font-awesome-icon icon="fas fa-envelope" />
+        <span class="description">Postal Code</span>
+      </label>
+      <span class="value">
+        <input
+            name="postalCode"
+            v-model="newValue.postalCode"
+            type="text"
+            placeholder="Postal Code" />
+      </span>
+    </div>
+
+    <div class="row">
+      <label for="country">
+        <font-awesome-icon icon="fas fa-flag" />
+        <span class="description">Country</span>
+      </label>
+      <span class="value">
+        <CountrySelector
+            name="country"
+            @input="newValue.country = $event"
+            :value="newValue.country || ''"
+            show-all />
+      </span>
+    </div>
+
+    <div class="row">
+      <label for="altitude">
+        <font-awesome-icon icon="fas fa-mountain" />
+        <span class="description">Altitude</span>
+      </label>
+      <span class="value">
+        <input
+            name="altitude"
+            v-model="newValue.altitude"
+            type="number"
+            step="0.01"
+            placeholder="Altitude" />
+      </span>
+    </div>
+
+    <div class="buttons">
+      <button type="submit"
+              title="Save"
+              :disabled="!hasChanged">
+        Save
+      </button>
+      <button type="button"
+              title="Cancel"
+              @click="$emit('close')">
+        Cancel
+      </button>
+    </div>
+  </form>
+</template>
+
+<script lang="ts">
+import Clipboard from '../mixins/Clipboard.vue';
+import CountrySelector from '../elements/CountrySelector.vue';
+import GPSPoint from '../models/GPSPoint';
+import UserDevice from '../models/UserDevice';
+
+export default {
+  emits: ['close', 'edit'],
+  mixins: [Clipboard],
+  components: {
+    CountrySelector,
+  },
+
+  props: {
+    device: {
+      type: [UserDevice, null],
+    },
+    point: {
+      type: [GPSPoint, null],
+    },
+  },
+
+  data() {
+    return {
+      hasChanged: false,
+      newValue: {...this.point} as GPSPoint,
+    };
+  },
+
+  watch: {
+    point: {
+      handler(newValue: GPSPoint) {
+        this.newValue = {...newValue} as GPSPoint;
+      },
+      immediate: true,
+    },
+
+    newValue: {
+      handler(newValue: GPSPoint) {
+        this.hasChanged = JSON.stringify(this.point) !== JSON.stringify(newValue);
+      },
+      deep: true,
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+form {
+  display: flex;
+  flex-direction: column;
+  // gap: 1rem;
+  padding: 1rem;
+
+  .row {
+    margin-bottom: 0.5rem;
+
+    label {
+      display: flex;
+      align-items: center;
+      font-size: 0.9rem;
+      color: var(--color-text);
+
+      .description {
+        opacity: 0.75;
+      }
+
+      svg {
+        margin-right: 0.5rem;
+        color: var(--color-accent);
+      }
+    }
+
+    .value {
+      $buttons-width: 2rem;
+      display: flex;
+      align-items: center;
+
+      input, textarea {
+        width: calc(100% - #{$buttons-width} - 1rem);
+        padding: 0.5rem;
+        border: 1px solid var(--color-border);
+        border-radius: 4px;
+        background-color: var(--color-background);
+        color: var(--color-text);
+        font-size: 0.9rem;
+
+        &:focus {
+          border-color: var(--color-accent);
+          outline: none;
+        }
+      }
+
+      .buttons {
+        width: $buttons-width;
+        display: flex;
+        align-items: center;
+        margin: 0 0 0 0.5rem;
+        padding: 0;
+
+        button {
+          height: 1.25rem;
+          background: none;
+          border: none;
+          color: var(--color-accent);
+          font-size: 0.9rem;
+          cursor: pointer;
+          padding: 0;
+
+          &:hover {
+            color: var(--color-accent-hover);
+          }
+
+          &:active {
+            color: var(--color-accent-active);
+          }
+        }
+      }
+    }
+  }
+
+  .buttons {
+    font-size: 0.85rem;
+    [type=submit] {
+      background: var(--color-accent);
+    }
+  }
+}
+</style>
diff --git a/frontend/src/components/PointInfo.vue b/frontend/src/components/PointInfo.vue
index ac8e2a9..7d5ca9d 100644
--- a/frontend/src/components/PointInfo.vue
+++ b/frontend/src/components/PointInfo.vue
@@ -29,8 +29,10 @@
           {{ speedKmH }} km/h
         </p>
 
+        <!-- @vue-ignore -->
         <form class="description editor" @submit.prevent="editPoint" v-if="editDescription">
           <div class="row">
+            <!-- @vue-ignore -->
             <textarea
               :value="point.description"
               @keydown.enter="editPoint"
@@ -87,18 +89,36 @@
         </p>
         <p class="timestamp" v-if="timeString">{{ timeString }}</p>
 
-        <div class="remove">
-          <button title="Remove" @click="$emit('remove', point)">
+        <div class="buttons">
+          <button title="Edit" class="edit" @click="showEditModal = true">
+            <font-awesome-icon icon="fas fa-edit" />&nbsp; Edit
+          </button>
+
+          <button title="Remove" class="remove" @click="$emit('remove', point)">
             <font-awesome-icon icon="fas fa-trash-alt" />&nbsp; Remove
           </button>
         </div>
       </div>
     </div>
+
+    <Modal :visible="showEditModal" @close="showEditModal = false">
+      <template #title>
+        Edit Point
+      </template>
+
+      <EditPointInfo
+        :device="device"
+        :point="newValue"
+        @close="showEditModal = false"
+        @edit="editPoint" />
+    </Modal>
   </div>
 </template>
 
 <script lang="ts">
+import EditPointInfo from './EditPointInfo.vue';
 import Map from 'ol/Map';
+import Modal from '../elements/Modal.vue';
 import Overlay from 'ol/Overlay';
 import type { TCountryCode } from 'countries-list';
 import { getCountryData, getEmojiFlag } from 'countries-list';
@@ -110,6 +130,11 @@ import UserDevice from '../models/UserDevice';
 export default {
   emit: ['close', 'edit', 'remove'],
   mixins: [Dates],
+  components: {
+    EditPointInfo,
+    Modal,
+  },
+
   props: {
     device: {
       type: [UserDevice, null],
@@ -121,9 +146,11 @@ export default {
 
   data() {
     return {
-      newValue: {} as GPSPoint,
+      // @ts-ignore
+      newValue: this.point ? new GPSPoint({ ...this.point }) : null as GPSPoint | null,
       editDescription: false,
       popup: null as Overlay | null,
+      showEditModal: false,
     }
   },
 
@@ -217,10 +244,22 @@ export default {
       map.addOverlay(this.popup)
     },
 
-    editPoint() {
-      this.newValue.description = (this.$refs.description as HTMLTextAreaElement).value
-      this.$emit('edit', this.newValue)
+    editPoint(newValue: GPSPoint | null | undefined) {
       this.editDescription = false
+
+      // If no new structured value is provided, then only edit the description
+      if (newValue == null) {
+        if (this.newValue) {
+          this.newValue.description = (this.$refs.description as HTMLTextAreaElement).value
+        }
+
+        this.$emit('edit', this.newValue)
+        return
+      }
+
+      // Otherwise, propagate the whole new value
+      this.newValue = newValue
+      this.$emit('edit', this.newValue)
     },
 
     onDescriptionBlur() {
@@ -408,17 +447,30 @@ export default {
     font-weight: bold;
     font-size: 0.9em;
   }
+}
 
-  .remove {
-    font-size: 0.85em;
-    margin-top: 0.5em;
+.buttons {
+  display: flex;
+  justify-content: space-between;
+  font-size: 0.85em;
+  margin-top: 0.5em;
 
-    button {
-      width: 100%;
-      background: none;
-      border: none;
+  button {
+    width: 100%;
+    background: none;
+    border: none;
+    margin-left: -0.5em;
+
+    &.edit {
+      color: var(--color-accent);
+
+      &:hover {
+        color: var(--color-accent-bg);
+      }
+    }
+
+    &.remove {
       color: var(--vt-c-red-fg-light);
-      margin-left: -0.5em;
 
       &:hover {
         color: var(--vt-c-red-fg-dark);
diff --git a/frontend/src/elements/Autocomplete.vue b/frontend/src/elements/Autocomplete.vue
index 467c3a1..888371d 100644
--- a/frontend/src/elements/Autocomplete.vue
+++ b/frontend/src/elements/Autocomplete.vue
@@ -80,7 +80,7 @@ export default {
       }
 
       let matches = this.values.filter((value: AutocompleteValue) =>
-        value.value.toLowerCase() === this.newValue.toLowerCase()
+        value?.value?.toLowerCase() === this.newValue.toLowerCase()
       ) as AutocompleteValue[];
 
       if (!matches.length) {
@@ -188,7 +188,7 @@ export default {
 
       if (newValue) {
         this.$nextTick(() => {
-          this.$refs.input.focus();
+          (this.$refs.input as HTMLInputElement).focus();
         });
       }
     },
diff --git a/frontend/src/elements/CountrySelector.vue b/frontend/src/elements/CountrySelector.vue
index f7aad57..22dc572 100644
--- a/frontend/src/elements/CountrySelector.vue
+++ b/frontend/src/elements/CountrySelector.vue
@@ -90,7 +90,9 @@ export default {
           if (visitedCountries[key]) {
             return acc
           }
-          acc[key] = this.toAutocompleteValue(countries[key])
+
+          // @ts-ignore
+          acc[key] = this.toAutocompleteValue(countries[key] as any)
           return acc
         }, {}
       )
diff --git a/frontend/src/elements/Modal.vue b/frontend/src/elements/Modal.vue
index 05667d7..a6ed168 100644
--- a/frontend/src/elements/Modal.vue
+++ b/frontend/src/elements/Modal.vue
@@ -36,6 +36,8 @@ export default {
 </script>
 
 <style lang="scss" scoped>
+@use '@/styles/common.scss' as *;
+
 .modal-container {
   position: fixed;
   top: 0;
@@ -49,13 +51,21 @@ export default {
   z-index: 1000;
 
   .modal {
-    min-width: 30em;
     background-color: var(--color-background);
     border-radius: 0.5em;
     box-shadow: 0 0 1em rgba(0, 0, 0, 0.5);
     overflow: hidden;
     animation: fade-in 0.5s;
 
+    @include until(tablet) {
+      min-width: 90vw;
+      max-width: 95vw;
+    }
+
+    @include from(tablet) {
+      min-width: 500px;
+    }
+
     .modal-header {
       display: flex;
       align-items: center;
diff --git a/frontend/src/mixins/Clipboard.vue b/frontend/src/mixins/Clipboard.vue
new file mode 100644
index 0000000..a93a304
--- /dev/null
+++ b/frontend/src/mixins/Clipboard.vue
@@ -0,0 +1,16 @@
+<script lang="ts">
+import Notifications from './Notifications.vue'
+
+export default {
+  mixins: [Notifications],
+  methods: {
+    async copyToClipboard(text: string) {
+      await navigator.clipboard.writeText(text)
+      this.notify({
+        content: 'Copied to the clipboard',
+        icon: 'copy',
+      })
+    },
+  },
+}
+</script>
diff --git a/frontend/src/mixins/Stats.vue b/frontend/src/mixins/Stats.vue
index 7040151..22eab30 100644
--- a/frontend/src/mixins/Stats.vue
+++ b/frontend/src/mixins/Stats.vue
@@ -1,10 +1,12 @@
 <script lang="ts">
 import type { Optional } from '../models/Types';
+import Api from './Api.vue';
 import Country from '../models/Country';
 import LocationStats from '../models/LocationStats';
 import StatsRequest from '../models/StatsRequest';
 
 export default {
+  mixins: [Api],
   methods: {
     async getCountries(): Promise<Country[]> {
       return (
@@ -21,6 +23,25 @@ export default {
       .map((record: LocationStats) => Country.fromCode(record.key.country))
       .filter((country: Optional<Country>) => !!country)
     },
+
+    async getLocalities(filter: {
+      country: string;
+      locality: string;
+      postalCode: string;
+    }): Promise<string[]> {
+      return (
+        await this.getStats(
+          new StatsRequest({
+            // @ts-ignore
+            userId: this.$root.user.id,
+            groupBy: ['locality'],
+            order: 'desc',
+          })
+        )
+      )
+      .filter((record: LocationStats) => !!record.key.locality)
+      .map((record: LocationStats) => record.key.locality)
+    },
   },
 }
 </script>