diff --git a/mo/email.py b/mo/email.py
index 597ce0044b60ff58f7e4f3c2e53f76f256362b83..812a7639b03c063d1a8dc929b94637fc1bba9f12 100644
--- a/mo/email.py
+++ b/mo/email.py
@@ -120,3 +120,26 @@ def send_confirm_change_email(user: db.User, token: str) -> bool:
 
         Váš OSMO
     '''.format(confirm_url('e', token))))
+
+
+def send_join_notify_email(dest: db.User, who: db.User, contest: db.Contest) -> bool:
+    round = contest.round
+    place = contest.place
+    if contest.round.enroll_mode == db.RoundEnrollMode.confirm:
+        confirm = 'Přihlášku je potřeba potvrdit v seznamu účastníků soutěže.'
+    else:
+        confirm = 'Přihláška byla schválena automaticky.'
+
+    return send_user_email(dest, f'Nový účastník kategorie {round.category}', textwrap.dedent(f'''\
+        Nový účastník se přihlásil do MO v oblasti, kterou garantujete.
+
+        Jméno:      {who.full_name()}
+        E-mail:     {who.email}
+        Kategorie:  {round.category}
+        Kolo:       {round.name}
+        Místo:      {place.name}
+
+        {confirm}
+
+        Váš OSMO
+    '''))
diff --git a/mo/web/__init__.py b/mo/web/__init__.py
index efde098980c9a047258e3961d29cdc2bb02b7421..b303ff34de05e973182bf138c8cf8bafc4fd772c 100644
--- a/mo/web/__init__.py
+++ b/mo/web/__init__.py
@@ -115,6 +115,7 @@ app.assets.add_assets([
     'bootstrap.min.css',
     'mo.css',
     'js/news-reloader.js',
+    'js/osmo.js',
 ])
 
 
diff --git a/mo/web/templates/user_contest.html b/mo/web/templates/user_contest.html
index 147bc9ab0432159148d4dbaf17a7d67d99bcde44..848ceac4682196f019e08b8e60e16f8b79443f4a 100644
--- a/mo/web/templates/user_contest.html
+++ b/mo/web/templates/user_contest.html
@@ -29,7 +29,18 @@
 	{% endif %}
 {% endif %}
 
-{% if state == RoundState.preparing %}
+{% if part_state == PartState.registered %}
+<p>
+	Vaše přihláška do této soutěže <b>dosud nebyla potvrzena organizátory.</b>
+	Vyčkejte prosím.
+	{% set state = RoundState.preparing %}
+</p>
+{% elif part_state == PartState.refused %}
+<p>
+	Vaše přihláška do této soutěže <b>byla odmítnuta organizátory.</b>
+	{% set state = RoundState.preparing %}
+</p>
+{% elif state == RoundState.preparing %}
 <p>
 	Soutěžní kolo se <b>připravuje</b>{% if round.ct_tasks_start and round.ct_tasks_start > g.now %},
 	začne <b>{{ round.ct_tasks_start|time_and_timedelta }}</b>{% endif %}.
@@ -64,12 +75,12 @@ Pokud si s tvorbou PDF nevíte rady, zkuste se podívat do <a href='https://docs
 <p>Soutěž se nachází v neznámém stavu. To by se nemělo stát :)
 {% endif %}
 
+{% if state != RoundState.preparing %}
+
 {% if contest.ct_task_statement_available() %}
 <p>Můžete si stáhnout <a href='{{ url_for('user_task_statement', id=contest.contest_id) }}'>zadání úloh</a>.
 {% endif %}
 
-{% if state != RoundState.preparing %}
-
 <h3>Úlohy</h3>
 
 <table class="table table-bordered table-hover">
diff --git a/mo/web/templates/user_index.html b/mo/web/templates/user_index.html
index 27e5b95e2e5372058f77a9fa9bbd0f852a974d15..440b9a16f664150213f456f110b2329d6680a3cd 100644
--- a/mo/web/templates/user_index.html
+++ b/mo/web/templates/user_index.html
@@ -42,4 +42,6 @@
 	<p>Momentálně se neúčastníte žádného kola MO.
 {% endif %}
 
+<p><a class="btn btn-primary" href="{{ url_for('user_join') }}">Přihlásit se do další kategorie</a>
+
 {% endblock %}
diff --git a/mo/web/templates/user_join_list.html b/mo/web/templates/user_join_list.html
new file mode 100644
index 0000000000000000000000000000000000000000..453771ec50e907bed4ce9711567f01d3a46ffd3a
--- /dev/null
+++ b/mo/web/templates/user_join_list.html
@@ -0,0 +1,38 @@
+{% extends "base.html" %}
+{% block title %}Přihláška do MO{% endblock %}
+{% block body %}
+
+{% if available_rounds %}
+	<p>Zde si můžete vybrat, do kterých kategorií olympíády se přihlásíte.
+
+	<table class="table">
+		<thead>
+			<tr>
+				<th>Kategorie
+				<th>Popis
+				<th>Kolo
+				<th>Odkazy
+		<tbody>
+			{% for round in available_rounds %}
+			<tr>
+				<td><b>{{ round.category }}</b>
+				<td>{{ round.enroll_advert }}</b>
+				<td>{{ round.name }}
+				{% if round.round_id in pcrs_by_round_id %}
+					<td>Již přihlášen
+				{% else %}
+					<td><a href='{{ url_for('user_join_round', round_id=round.round_id) }}' class='btn btn-xs btn-primary'>Přihlásit se</a>
+				{% endif %}
+			{% endfor %}
+	</table>
+{% else %}
+	<p>V tomto školním roce zatím nejsou otevřené žádné kategorie olympiády.
+	Zkuste to prosím později.
+{% endif %}
+
+{% if pcrs_by_round_id %}
+{# Není-li účastník přihlášen v žádné soutěží, user_index přesměrovává zpět na tuto stránku. #}
+<p><a class="btn btn-default" href="{{ url_for('user_index') }}">Zpět</a>
+{% endif %}
+
+{% endblock %}
diff --git a/mo/web/templates/user_join_round.html b/mo/web/templates/user_join_round.html
new file mode 100644
index 0000000000000000000000000000000000000000..d11437659439ccec3c18258a53db3b52b894a7a8
--- /dev/null
+++ b/mo/web/templates/user_join_round.html
@@ -0,0 +1,71 @@
+{% extends "base.html" %}
+{% import "bootstrap/wtf.html" as wtf %}
+{% block head %}
+<script src='{{ asset_url('js/osmo.js') }}'></script>
+<script>
+	osmo_api_root = {{ url_for('api_root')|jsescape }};
+	osmo_school = new OSMOSchool();
+	{% if form.town_query.data %}
+		osmo_school.prefill_town_query = {{ form.town_query.data|jsescape }};
+		{% if form.town_list.data %}
+		osmo_school.prefill_town_list = {{ form.town_list.data|jsescape }};
+		{% if form.school.data %}
+		osmo_school.prefill_school = {{ form.school.data|jsescape }};
+		{% endif %}
+		{% endif %}
+	{% endif %}
+	document.addEventListener('DOMContentLoaded', () => { osmo_school.init() });
+</script>
+{% endblock %}
+{% block title %}Přihláška{% endblock %}
+{% block body %}
+
+<h3 style='margin-bottom: 21px'>{{ round.name }} kategorie {{ round.category }} {% if round.enroll_advert %} ({{ round.enroll_advert }}){% endif %}</h3>
+
+<form action="" method="POST" role="form" class="form form-horizontal">
+	{{ form.csrf_token }}
+	{% if form.school %}
+		<div id='town_query-group' class='form-group row required{% if form.school.errors %} has-error{% endif %}'>
+			<label class='col-sm-2 control-label' for='town_query'>Škola</label>
+			<div class='col-sm-8'>
+				<input autofocus="" class="form-control" id="town_query" name="town_query" type="text" value="">
+				<div id='town_query-help' class='help-block'>
+					Zadejte prvních pár znaků jména obce a zmáčkněte Hledat.
+					Pokud se vám nedaří školu najít (například proto, že studujete v zahraničí),
+					informujte prosím <a href='{{ url_for('doc_about') }}'>správce OSMO</a>.
+				</div>
+			</div>
+			<div class='col-sm-2'>
+				<button class='btn btn-primary' type='button' onclick='osmo_school.find_town(false)'>
+					Hledat
+				</button>
+			</div>
+		</div>
+		<div id='town_list-div' class='form-group row' style='display: none'>
+			<div class='col-sm-offset-2 col-sm-10'>
+				<select id='town_list' name='town_list' onchange='osmo_school.town_picked()'>
+				</select>
+			</div>
+		</div>
+		<div id='school-div' name='school' class='form-group row' style='display: none'>
+			<div class='col-sm-offset-2 col-sm-10'>
+				<select id='school' name='school'>
+				</select>
+			</div>
+		</div>
+		{{ wtf.form_field(form.grade, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
+		{{ wtf.form_field(form.birth_year, form_type='horizontal', horizontal_columns=('sm', 2, 10)) }}
+		<p>Přihlášením do soutěže udělujete souhlas se <a href='{{ url_for('doc_gdpr') }}'>zpracováním osobních údajů</a>.
+		U nezletilých účastníků musí přihlášku odeslat zákonný zástupce.
+	{% else %}
+		<p>Vaše osobní údaje už známe z ostatních kategorií. Stačí tedy potvrdit přihlášení.
+	{% endif %}
+	<div class='form-group'>
+		<div class='col-sm-12'>
+			{{ wtf.form_field(form.submit, form_type='inline', button_map={'submit': 'primary'}) }}
+			<a class="btn btn-default" href="{{ url_for('user_join') }}">Zpět</a>
+		</div>
+	</div>
+</form>
+
+{% endblock %}
diff --git a/mo/web/user.py b/mo/web/user.py
index fa8fb961fd860324870fed854b6474b3f3780708..9d77622d73f3a1e7dbf5e0679ff9cfc01dd7f780 100644
--- a/mo/web/user.py
+++ b/mo/web/user.py
@@ -1,4 +1,4 @@
-from flask import render_template, jsonify, g, redirect, url_for, flash
+from flask import render_template, jsonify, g, redirect, url_for, flash, request
 from flask_wtf import FlaskForm
 import flask_wtf.file
 from sqlalchemy import and_
@@ -6,15 +6,18 @@ from sqlalchemy.orm import joinedload
 from typing import List, Tuple
 import werkzeug.exceptions
 import wtforms
+from wtforms.validators import Required
 
 import mo
 import mo.config as config
+import mo.email
 import mo.db as db
 import mo.submit
 import mo.util
 from mo.util import logger
 from mo.util_format import time_and_timedelta
 from mo.web import app
+import mo.web.fields as mo_fields
 import mo.web.util
 
 
@@ -22,6 +25,9 @@ import mo.web.util
 def user_index():
     pcrs = load_pcrs()
 
+    if not pcrs:
+        return redirect(url_for('user_join'))
+
     return render_template(
         'user_index.html',
         pions=pcrs,
@@ -36,11 +42,210 @@ def load_pcrs() -> List[Tuple[db.Participation, db.Contest, db.Round]]:
             .filter(db.Participation.user == g.user)
             .filter(db.Round.year == mo.current_year)
             .options(joinedload(db.Contest.place))
-            .order_by(db.Round.year.desc(), db.Round.category, db.Round.seq, db.Round.part)
+            .order_by(db.Round.category, db.Round.seq, db.Round.part)
             .all())
 
 
-def get_contest(id: int) -> db.Contest:
+@app.route('/user/join/')
+def user_join():
+    available_rounds: List[db.Round] = (
+        db.get_session().query(db.Round)
+        .select_from(db.Round)
+        .filter_by(year=mo.current_year)
+        .filter(db.Round.enroll_mode.in_([db.RoundEnrollMode.register, db.RoundEnrollMode.confirm]))
+        .filter_by(state=db.RoundState.running)
+        .order_by(db.Round.category, db.Round.seq)
+        .all())
+    available_rounds = [r for r in available_rounds if not r.is_subround()]
+
+    pcrs = load_pcrs()
+    pcrs_by_round_id = {pcr[1].round_id: pcr for pcr in pcrs}
+
+    return render_template(
+        'user_join_list.html',
+        available_rounds=available_rounds,
+        pcrs_by_round_id=pcrs_by_round_id,
+    )
+
+
+class JoinRoundForm(FlaskForm):
+    # Zadávání školy je JS hack implementovaný v šabloně. Fields definují jen rozhraní.
+    school = mo_fields.School("Škola", validators=[Required()])
+    town_query = wtforms.HiddenField()
+    town_list = wtforms.HiddenField()
+
+    grade = mo_fields.Grade("Třída", validators=[Required()])
+    birth_year = mo_fields.BirthYear("Rok narození", validators=[Required()])
+    submit = wtforms.SubmitField('Přihlásit se')
+
+
+@app.route('/user/join/<int:round_id>/', methods=('GET', 'POST'))
+def user_join_round(round_id):
+    sess = db.get_session()
+    round = sess.query(db.Round).get(round_id)
+    if not round:
+        raise werkzeug.exceptions.NotFound()
+
+    if (round.year != mo.current_year
+            or round.part != 0
+            or round.enroll_mode not in [db.RoundEnrollMode.register, db.RoundEnrollMode.confirm]
+            or round.state != db.RoundState.running):
+        flash('Do této kategorie se není možné přihlásit.', 'danger')
+        return redirect(url_for('user_register'))
+
+    pion = (sess.query(db.Participation)
+            .select_from(db.Participation)
+            .filter_by(user=g.user)
+            .join(db.Participation.contest)
+            .filter(db.Contest.round == round)
+            .with_for_update()
+            .one_or_none())
+    if pion:
+        flash('Do této kategorie už jste přihlášen.', 'info')
+        return redirect(url_for('user_join'))
+
+    pant = (sess.query(db.Participant)
+            .filter_by(user=g.user, year=round.year)
+            .with_for_update()
+            .one_or_none())
+
+    form = JoinRoundForm()
+    if pant:
+        del form.school
+        del form.grade
+        del form.birth_year
+
+    if form.validate_on_submit():
+        if form.submit.data:
+            if not pant:
+                pant = join_create_pant(form)
+                sess.add(pant)
+            contest = join_create_contest(round, pant)
+            join_create_pion(contest)
+            sess.commit()
+            join_notify(contest)
+
+            msg = 'Přihláška přijata.'
+            if round.enroll_mode == db.RoundEnrollMode.confirm:
+                msg += ' Ještě ji musí potvrdit organizátor soutěže.'
+            flash(msg, 'success')
+            return redirect(url_for('user_index'))
+    elif not pant and request.method == 'GET':
+        # Pokusíme se předvyplnit data z minulých ročníků
+        prev_pant = (sess.query(db.Participant)
+                     .filter_by(user=g.user)
+                     .options(joinedload(db.Participant.school_place, db.Place.parent_place))
+                     .order_by(db.Participant.year.desc())
+                     .limit(1).one_or_none())
+        if prev_pant:
+            form.school.data = f'#{prev_pant.school}'
+            town = prev_pant.school_place.parent_place
+            form.town_query.data = town.name
+            form.town_list.data = str(town.place_id)
+            form.birth_year.data = prev_pant.birth_year
+
+    return render_template(
+        'user_join_round.html',
+        round=round,
+        form=form,
+    )
+
+
+def join_create_pant(form: JoinRoundForm) -> db.Participant:
+    assert form.school.place is not None
+    pant = db.Participant(user=g.user,
+                          year=mo.current_year,
+                          school_place=form.school.place,
+                          grade=form.grade.data,
+                          birth_year=form.birth_year.data)
+
+    logger.info(f'Join: Účastník #{g.user.user_id} se přihlásil do {pant.year}. ročníku')
+    mo.util.log(
+        type=db.LogType.participant,
+        what=g.user.user_id,
+        details={'action': 'create-participant', 'reason': 'user-join', 'new': db.row2dict(pant)},
+    )
+
+    return pant
+
+
+def join_create_contest(round: db.Round, pant: db.Participant) -> db.Contest:
+    sess = db.get_session()
+
+    place = pant.school_place
+    if place.level != round.level:
+        parents = db.get_place_parents(pant.school_place)
+        places = [p for p in parents if p.level == round.level]
+        assert len(places) == 1
+        place = places[0]
+    # XXX: Z rekurzivního dotazu nedostaneme plnohodnotný db.Place, ale jenom named tuple, tak musíme pracovat s ID.
+    place_id = place.place_id
+
+    assert round.part == 0   # U kol s více částmi bude potřeba založit všechny contesty, ale přihlásit jen do primárního
+    c = (sess.query(db.Contest)
+         .filter_by(round=round, place_id=place_id)
+         .with_for_update()
+         .one_or_none())
+    if not c:
+        c = db.Contest(
+            round=round,
+            place_id=place_id,
+            state=db.RoundState.running,
+        )
+        sess.add(c)
+        sess.flush()
+        c.master = c
+
+        logger.info(f'Join: Automaticky založena soutěž #{c.contest_id} {round.round_code()} pro místo #{place_id}')
+        mo.util.log(
+            type=db.LogType.contest,
+            what=c.contest_id,
+            details={'action': 'created', 'reason': 'user-join'},
+        )
+
+    return c
+
+
+def join_create_pion(c: db.Contest) -> None:
+    sess = db.get_session()
+
+    if c.round.enroll_mode == db.RoundEnrollMode.register:
+        state = db.PartState.active
+    else:
+        state = db.PartState.registered
+    p = db.Participation(user=g.user, contest=c, place=c.place, state=state)
+    sess.add(p)
+
+    logger.info(f'Join: Účastník #{g.user.user_id} přihlášen do soutěže #{c.contest_id}')
+    mo.util.log(
+        type=db.LogType.participant,
+        what=g.user.user_id,
+        details={'action': 'add-to-contest', 'reason': 'user-join', 'new': db.row2dict(p)},
+    )
+
+
+def join_notify(c: db.Contest) -> None:
+    sess = db.get_session()
+    r = c.round
+    place = c.place
+    while place is not None:
+        uroles = (sess.query(db.UserRole)
+                  .filter(db.UserRole.role.in_((db.RoleType.garant, db.RoleType.garant_kraj, db.RoleType.garant_okres, db.RoleType.garant_skola)))
+                  .filter_by(place_id=place.place_id)
+                  .options(joinedload(db.UserRole.user))
+                  .all())
+        notify = [ur.user for ur in uroles if ur.applies_to(at=place, year=r.year, cat=r.category, seq=r.seq)]
+        if notify:
+            for org in notify:
+                logger.info(f'Join: Notifikuji orga <{org.email}> pro místo {place.get_code()}')
+                mo.email.send_join_notify_email(org, g.user, c)
+            return
+        place = place.parent_place
+
+    logger.warn('Join: Není komu poslat mail')
+
+
+def get_contest_pion(id: int, require_reg: bool = True) -> Tuple[db.Contest, db.Participation]:
     contest = (db.get_session().query(db.Contest)
                .options(joinedload(db.Contest.place),
                         joinedload(db.Contest.round))
@@ -49,13 +254,20 @@ def get_contest(id: int) -> db.Contest:
     if not contest:
         raise werkzeug.exceptions.NotFound()
 
-    # FIXME: Kontrolovat nějak pion.state?
     pion = (db.get_session().query(db.Participation)
             .filter_by(user=g.user, contest_id=contest.master_contest_id)
             .one_or_none())
     if not pion:
         raise werkzeug.exceptions.Forbidden()
 
+    if require_reg and pion.state in [db.PartState.registered, db.PartState.refused]:
+        raise werkzeug.exceptions.Forbidden()
+
+    return contest, pion
+
+
+def get_contest(id: int) -> db.Contest:
+    contest, _ = get_contest_pion(id)
     return contest
 
 
@@ -72,7 +284,7 @@ def get_task(contest: db.Contest, id: int) -> db.Task:
 @app.route('/user/contest/<int:id>/')
 def user_contest(id: int):
     sess = db.get_session()
-    contest = get_contest(id)
+    contest, pion = get_contest_pion(id, require_reg=False)
 
     messages = sess.query(db.Message).filter_by(round_id=contest.round_id).order_by(db.Message.created_at).all()
 
@@ -88,6 +300,7 @@ def user_contest(id: int):
     return render_template(
         'user_contest.html',
         contest=contest,
+        part_state=pion.state,
         task_sols=task_sols,
         messages=messages,
         max_submit_size=config.MAX_CONTENT_LENGTH,
diff --git a/static/js/osmo.js b/static/js/osmo.js
new file mode 100644
index 0000000000000000000000000000000000000000..02605c9ebe58004a6cd636d08056304cad4bf116
--- /dev/null
+++ b/static/js/osmo.js
@@ -0,0 +1,154 @@
+/*
+ *  JavaScriptové funkce pro OSMO
+ */
+
+'use strict';
+
+let osmo_api_root = undefined;
+
+/*** Výběr škol ***/
+
+class OSMOSchool {
+	constructor() {
+		this.prefill_town_query = undefined;
+		this.prefill_town_list = undefined;
+		this.prefill_school = undefined;
+	}
+
+	find_town_error(msg) {
+		document.getElementById('town_query-group').classList.add('has-error');
+		document.getElementById('town_query-help').innerText = msg;
+	}
+
+	async do_find_town(query) {
+		let resp = undefined;
+		try {
+			resp = await fetch(osmo_api_root + 'find-town?q=' + encodeURIComponent(query));
+		} catch (err) {
+			console.log('OSMO: Search failed: ' + err);
+			this.find_town_error('Spojení se serverem selhalo.');
+			return;
+		}
+
+		if (resp.status !== 200) {
+			console.log('OSMO: Search status: ' + resp.status);
+			this.find_town_error('Spojení se serverem selhalo.');
+			return;
+		}
+
+		const ans = await resp.json();
+		if (ans.error !== undefined) {
+			this.find_town_error(ans.error);
+			return;
+		}
+
+		const list = document.getElementById('town_list');
+		const opts = list.options;
+		opts.length = 0;
+		opts.add(new Option('Vyberte obec ze seznamu', ""));
+		for (const t of ans.found) {
+			opts.add(new Option(t[1], t[0]));
+		}
+
+		document.getElementById('town_list-div').style.display = 'block';
+		document.getElementById('town_query-help').innerText = ans.msg
+
+		if (this.prefill_town_list !== undefined) {
+			list.value = this.prefill_town_list;
+			this.prefill_town_list = undefined;
+			this.town_picked();
+		} else if (ans.found.length == 1) {
+			list.selectedIndex = 1;
+			this.town_picked();
+		}
+	}
+
+	find_town(during_init) {
+		const query = document.getElementById('town_query').value;
+
+		document.getElementById('town_list-div').style.display = 'none';
+		document.getElementById('school-div').style.display = 'none';
+		document.getElementById('town_query-help').innerText = 'Hledám...';
+		if (!during_init) {
+			document.getElementById('town_query-group').classList.remove('has-error');
+		}
+
+		this.do_find_town(query);
+	}
+
+	town_keydown(event) {
+		if (event.key === 'Enter') {
+			event.preventDefault();
+			this.find_town(false);
+		}
+	}
+
+	async do_get_schools(town_id) {
+		let resp = undefined;
+		try {
+			resp = await fetch(osmo_api_root + 'get-schools?town=' + encodeURIComponent(town_id));
+		} catch (err) {
+			console.log('OSMO: Search failed: ' + err);
+			return;
+		}
+
+		if (resp.status !== 200) {
+			console.log('OSMO: Search status: ' + resp.status);
+			return;
+		}
+
+		const ans = await resp.json();
+		const list = document.getElementById('school');
+		const opts = list.options;
+		list.replaceChildren();
+		if (ans.zs.length > 0 || ans.ss.length > 0) {
+			opts.add(new Option('Vyberte školu ze seznamu', ""));
+			if (ans.zs.length > 0) {
+				const g = document.createElement('optgroup');
+				g.label = 'Základní školy'
+				for (const s of ans.zs) {
+					g.append(new Option(s.name, '#' + s.id));
+				}
+				opts.add(g);
+			}
+			if (ans.ss.length > 0) {
+				const g = document.createElement('optgroup');
+				g.label = 'Střední školy'
+				for (const s of ans.ss) {
+					g.append(new Option(s.name, '#' + s.id));
+				}
+				opts.add(g);
+			}
+		} else {
+			opts.add(new Option('V této obci nejsou žádné školy.', ""));
+		}
+
+		document.getElementById('school-div').style.display = 'block';
+
+		if (this.prefill_school !== undefined) {
+			list.value = this.prefill_school;
+			this.prefill_school = undefined;
+		}
+	}
+
+	town_picked() {
+		const town_id = document.getElementById('town_list').value;
+		document.getElementById('school-div').style.display = 'none';
+		if (town_id != "") {
+			this.do_get_schools(town_id);
+		}
+	}
+
+	init() {
+		console.log('OSMO: Init schools');
+
+		var tq = document.getElementById('town_query');
+		tq.addEventListener('keydown', (event) => { this.town_keydown(event); });
+
+		if (this.prefill_town_query !== undefined) {
+			tq.value = this.prefill_town_query;
+			this.prefill_town_query = undefined;
+			this.find_town(true);
+		}
+	}
+}