Meta box not saving spaces

The answer is simple: do not use sanitize_html_class. Take the input as it is (you can still do basic validation like length, making sure it is an integer only etc) and if you need to output that on the front and you don’t trust the user who stored the data either use something like esc_html or wp_strip_all_tags depending on your needs. Of course, you can use wp_strip_all_tags when processing the meta data too but how confused will the user be when noticing the content isn’t the same after saving the post? For this reason you may want to add a notice to not include any HTML tags.

As a side note, if you actually go ahead and add a custom field with the built-in WordPress mechanism you will see that it doesn’t strip anything, a value is stored as it is.

Since you’re still having troubles, here is your code a bit refactored:

function city_save_meta( $post_id, $post ) {
    if ( !isset( $_POST['city_nonce']) || !wp_verify_nonce( $_POST['city_nonce'], basename(__FILE__) ) )
        return $post_id;

    $post_type = get_post_type_object( $post->post_type);

    if ( !current_user_can( $post_type->cap->edit_post, $post_id ) )
        return $post_id;

    $new_meta_value = isset( $_POST['city_id'] ) ? $_POST['city_id'] : '';

    $meta_key = 'city_id';

    $meta_value = get_post_meta( $post_id, $meta_key, true );

    if ( ! empty( $new_meta_value ) ) {
        update_post_meta( $post_id, $meta_key, $new_meta_value );
    } else {
        /* Do you really expect to have multiple meta keys named exactly the same ('city_id')? */
        /* If you don't, you can skip the third parameter from 'delete_post_meta'. */
        delete_post_meta( $post_id, $meta_key, $meta_value );
    }
}